Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSameLine": false,
"printWidth": 120,
"bracketSpacing": true
}
52 changes: 39 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
# GlobalWebIndex Engineering Challenge
**Cat Lover App**

## Exercise: CatLover
Overview

Create a React application for cat lovers which is going to build upon thecatapi.com and will have 3 views.
The **first** view displays a list of 10 random cat images and a button to load more. Clicking on any of those images opens a modal view with the image and the information about the cat’s breed if available. This would be a link to the second view below - the breed detail. The modal should also contain a form to mark the image as your favourite (a part of the third view as well). Make sure you can copy-paste the URL of the modal and send it to your friends - they should see the same image as you can see.
This application is a React-based web app for managing and displaying cat images. Users can browse cat images, view detailed information about cat breeds, and manage their favorite images. The app integrates with a backend API for fetching data and supports modern React features such as hooks and context for state management.

The **second** view displays a list of cat breeds. Each breed opens a modal again with a list of cat images of that breed. Each of those images must be a link to the image detail from the previous point.

The **third** view allows you do the following things:
**API Integration**

- Display your favourite cats
- Remove an image from your favourites (use any UX option you like)
The app integrates with a backend API to fetch and manage data.

You can find the API documentation here: https://developers.thecatapi.com/
We give you a lot of freedom in technologies and ways of doing things. We only insist on you using React.js. Get creative as much as you want, we WILL appreciate it. You will not be evaluated based on how well you follow these instructions, but based on how sensible your solution will be. In case you are not able to implement something you would normally implement for time reasons, make it clear with a comment.
**Endpoints**

## Submission
- GET /breeds: Fetches a list of cat breeds.

Once you have built your app, share your code in the mean suits you best
Good luck, potential colleague!
- GET /images/search: Fetches random cat images.

- GET /favourites: Fetches the user's favorite cat images.

- POST /favourites: Adds an image to the user's favorites.

- DELETE /favourites/:id: Removes an image from the user's favorites.

**Axios Instance (apiClient.ts)**

- Configures a centralized Axios instance with base URL and headers for API requests.

- Includes error handling via interceptors

**Features**

1. Image Gallery: Display a grid of cat images with a "Load More" functionality.

2. Favorites Management: Add or remove cat images from favorites.

3. Breed Information: View detailed information about specific cat breeds.

4. Error Handling: Provides user-friendly error messages.

5. Responsive Design: Utilizes react-bootstrap for a consistent, responsive UI.

6. Global State Management: Shares the favorites state across components using the Context API.

**Installation and Setup**

1. yarn install
2. yarn start
55 changes: 55 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "platform-react-challenge",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"axios": "^1.7.9",
"bootstrap": "^5.3.3",
"react": "18",
"react-bootstrap": "^2.10.8",
"react-dom": "18",
"react-error-boundary": "^5.0.0",
"react-router-dom": "^7.1.3",
"react-scripts": "5.0.1",
"react-window": "^1.8.11",
"typescript": "^4.4.2",
"web-vitals": "^2.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-modal": "^3.16.3",
"@types/react-router-dom": "^5.3.3",
"@types/react-window": "^1.8.8",
"prettier": "3.4.2"
}
}
Binary file added public/favicon.ico
Binary file not shown.
43 changes: 43 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.

Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file added public/logo192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/logo512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
3 changes: 3 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
4 changes: 4 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.App {
text-align: center;
}

35 changes: 35 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Route, Routes } from 'react-router-dom';
import { lazy, Suspense } from 'react';

import Home from './pages/Home/Home';
import Layout from './Layout/Layout';
import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary';
import { ROUTES } from './constants';

import './App.css';

// Lazy loading non-critical components
const Breeds = lazy(() => import('./pages/Breeds/Breeds'));
const Favorites = lazy(() => import('./pages/Favorites/Favorites'));
const NotFound = lazy(() => import('./pages/NotFound/NotFound'));
const SpinnerComponent = lazy(() => import('./components/SpinnerComponent/SpinnerComponent'));

const App = () => (
<div className="App">
<Layout>
<ErrorBoundary>
<Suspense fallback={<SpinnerComponent />}>
<Routes>
<Route path={ROUTES.HOME} element={<Home />} />
<Route path={`${ROUTES.BREEDS}/:id?`} element={<Breeds />} />
<Route path={`${ROUTES.IMAGE}/:id?`} element={<Home />} />
<Route path={ROUTES.FAVORITES} element={<Favorites />} />
<Route path={ROUTES.NOT_FOUND} element={<NotFound />} />
</Routes>
</Suspense>
</ErrorBoundary>
</Layout>
</div>
);

export default App;
31 changes: 31 additions & 0 deletions src/Layout/Layout.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.custom-navbar {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 10px 20px;
}

.navbar-brand {
font-size: 1.5rem;
font-weight: bold;
color: var(--navColor) !important;
cursor: pointer;
}

.navbar-link {
font-size: 1.2rem;
margin: 0 10px;
transition: color 0.3s ease;
color: var(--navColor) !important;
}

.navbar-brand:hover, .navbar-link:hover {
color: #8e9d76 !important;
}

.content {
margin-top: 5rem;
}

.custom-toggler {
background-color: var(--navColor) !important
}

69 changes: 69 additions & 0 deletions src/Layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useState, FC, ReactNode, useEffect } from 'react';

import { Navbar, Nav, Container, Offcanvas } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';

import './Layout.css';

interface LayoutProps {
children?: ReactNode;
}

/**
* Layout component that provides a navigation bar and main content area.
* Includes responsive navigation with an offcanvas menu for smaller screens.
* @param children - The components to render inside the layout's content area.
*/
const Layout: FC<LayoutProps> = ({ children }) => {
const navigate = useNavigate();
const [showOffcanvas, setShowOffcanvas] = useState(false);

const handleNavClick = (redirectTo: string) => {
navigate(redirectTo);
setShowOffcanvas(false);
};

const toggleOffcanvas = () => setShowOffcanvas(!showOffcanvas);
const closeOffcanvas = () => setShowOffcanvas(false);

useEffect(() => {
document.title = 'Cat Lover App';
}, []);

return (
<>
<Navbar bg="dark" expand="lg" fixed="top" className="custom-navbar">
<Container>
<Navbar.Brand onClick={() => navigate('/')} className="navbar-brand">
Cat Lover App
</Navbar.Brand>
<Navbar.Toggle onClick={toggleOffcanvas} aria-controls="offcanvasNavbar" className="custom-toggler" />
<Navbar.Offcanvas
show={showOffcanvas}
onHide={closeOffcanvas}
id="offcanvasNavbar"
aria-labelledby="offcanvasNavbarLabel"
placement="end"
>
<Offcanvas.Header closeButton>
<Offcanvas.Title id="offcanvasNavbarLabel">Navigation</Offcanvas.Title>
</Offcanvas.Header>
<Offcanvas.Body>
<Nav className="justify-content-end flex-grow-1 pe-3">
<Nav.Link onClick={() => handleNavClick('breeds')} className="navbar-link">
Breeds
</Nav.Link>
<Nav.Link onClick={() => handleNavClick('favorites')} className="navbar-link">
Favorites
</Nav.Link>
</Nav>
</Offcanvas.Body>
</Navbar.Offcanvas>
</Container>
</Navbar>
<Container className="content">{children}</Container>
</>
);
};

export default Layout;
20 changes: 20 additions & 0 deletions src/apiClient/apiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import axios from 'axios';

const apiKey = 'live_4yaPeUDj6HG4aQbbQwPa7rk1MCuNR835rhVQkmcatbdlFk3oxmoRXlILFsZ5YsQn';

const axiosInstance = axios.create({
baseURL: 'https://api.thecatapi.com/v1',
headers: {
'x-api-key': apiKey,
},
timeout: 10000,
});

axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
throw error.response?.data?.message || error.message || 'An error occurred';
}
);

export default axiosInstance;
Loading