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
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

VITE_API_URL = https://api.thecatapi.com/v1/

VITE_API_KEY =
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist
node_modules
build
coverage
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "avoid",
"printWidth": 100,
"tabWidth": 2,
"endOfLine": "auto"
}
246 changes: 233 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,241 @@
# GlobalWebIndex Engineering Challenge
# 🐱 GWI Cat Lover App - Lefteris Katmadas

## Exercise: CatLover
A responsive, mobile first, themeable, and accessible cat image gallery built with modern React tools. Developed as part of a technical interview for GWI.

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.
---

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.
## Demo

The **third** view allows you do the following things:
[https://gwinew.webappcreative.co/](https://gwinew.webappcreative.co/)

- Display your favourite cats
- Remove an image from your favourites (use any UX option you like)
## 🚀 Tech Stack

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.
This project uses a modern frontend toolchain focused on performance, accessibility, and developer experience:

## Submission
- **React 19** – App foundation

Once you have built your app, share your code in the mean suits you best
Good luck, potential colleague!
- **Vite** – Fast dev/build tooling

- **TypeScript** – Type safety and DX

- **React Router DOM v7** – Client-side routing

- **TanStack React Query v5** – Data fetching & caching

- **Axios** – HTTP client

- **Emotion** – Themeable CSS-in-JS styling

- **Lodash.debounce** – Optimized input handling

- **React Content Loader** – Skeletons during data loading

---

## 🧹 Code Quality & Formatting

This project uses:

- **ESLint** – For static code analysis and best practices

- **Prettier** – For consistent code formatting

- **TypeScript ESLint Plugin** – For advanced TypeScript linting

**Useful scripts:**

```bash

# Lint all files

npm run lint



# Fix lint issues automatically

npm run lint:fix



# Format code using Prettier

npm run format

```

> The project is linted and formatted consistently to ensure maintainability and readability.

---

## ♿ Accessibility (a11y)

The portal is built with accessibility in mind and validated using **axe DevTools** to catch potential issues and ensure keyboard navigability and semantic markup.

---

## 🎨 Theming

The app supports **light** and **dark** themes using `@emotion/react`. Users can toggle between themes, and preferences are **persisted in localStorage**.

```ts
localStorage.setItem('theme', 'dark') // or 'light'
```

---

## 🧪 Testing Strategy

This project implements both **unit testing** and **end-to-end (E2E) testing**.

### ✅ Unit Tests

- **Framework:** [Vitest](https://vitest.dev/)

- **Libraries:**

- `@testing-library/react`

- `@testing-library/jest-dom`

- `@testing-library/user-event`

> ⚠️ Full unit coverage is not implemented. However, existing tests demonstrate structure, conventions, and testing potential.

**Run unit tests:**

```bash

npm run test

# or with coverage

npm run test:coverage

```

---

### 🧪 E2E Tests

- **Tool:** [Cypress](https://www.cypress.io/)

- E2E tests cover both homepage (`home.cy.ts`) and breed results (`breeds.cy.ts`)

- Scenarios tested include:

- UI rendering and interactions

- Modal behavior

- API-driven content loading

**Run Cypress:**

```bash

# GUI mode

npm run cypress:open



# Headless mode

npm run cypress:run

```

---

## 📁 Project Structure

```

src/

├── components/ # Reusable UI elements

├── views/ # Feature-based pages

├── types/ # Shared types

├── api/ # Axios setup and endpoints

├── hooks/ # Custom React hooks

├── styles/ # Emotion themes and global styles

├── tests/ # Unit tests

```

---

## ⚙️ Getting Started

### Prerequisites

- Node.js v18+

- npm

### Setup

```bash

git clone https://github.com/lkatmadas/platform-react-challenge

yarn install

yarn dev

```

Open your browser to [http://localhost:5173](http://localhost:5173).

---

## 🔍 API Usage Tip

To improve the relevance of initial results (e.g., to get images with breed details early on), you can modify the image search endpoint like so:

```ts
;`${API_ENDPOINTS.images.search}?limit=${limit}&page=${page}&has_breeds=true&order=${order}&size=${size}&mime_types=${mime_types}&format=json`
```

Setting `has_breeds` to `true` helps ensure that returned images include breed metadata.

---

## 🔐 Environment Variables

A `.env` file is used to store environment config, including the API key for TheCatAPI.

> **⚠️ Note:** The `.env` file is excluded from version control. The `VITE_API_KEY` is intentionally not shared. If needed, I can provide a valid API key privately for demonstration purposes.

---

## 📌 What Could Be Improved

- Increase unit test coverage and CI test reports

- Add integration tests for key flows

- Internationalization (i18n) support

---

## 📝 Summary

This project demonstrates:

- Modern tech adoption and clean architecture

- Accessible, testable UI practices

- Production-level structure with extensibility in mind

- Theme management with persistence

Feel free to reach out for any clarifications or code walkthroughs
7 changes: 7 additions & 0 deletions cypress.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'cypress'

export default defineConfig({
e2e: {
baseUrl: 'http://localhost:5173',
},
})
63 changes: 63 additions & 0 deletions cypress/e2e/breeds.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
describe('🐾 Cat Breeds Page', () => {
beforeEach(() => {
cy.visit('/breeds')
})

it('displays first 2 cat breed items with thumbnail and title', () => {
cy.wait(2000) // wait for breed data to load

cy.get('[data-testid="cat-breed-item"]')
.should('have.length.at.least', 2)
.then($items => {
// Limit to first 2 items
const firstTwo = Cypress.$($items).slice(0, 2)

cy.wrap(firstTwo).each($item => {
cy.wrap($item)
.find('img[data-testid="breed-thumbnail"]')
.should('exist')
.and('have.attr', 'src')
.and('include', 'cdn2.thecatapi.com')

cy.wrap($item).find('h2').should('exist').and('not.be.empty')
})
})
})

it('opens modal when the first breed item is clicked and shows breed image', () => {
cy.get('[data-testid="cat-breed-item"]').first().click()
cy.get('[data-testid="modal"]').should('exist').and('be.visible')
cy.get('[data-testid="breed-cat-image"]', { timeout: 5000 }).should('exist').and('be.visible')
})

it('shows action button on hover over breed image', () => {
cy.visit('/breeds')
cy.wait(2000)
cy.get('[data-testid="cat-breed-item"]').first().click()
cy.get('[data-testid="modal"]').should('exist').and('be.visible')
cy.get('[data-testid="breed-cat-image"]').first().find('[data-testid="button"]').click()
})

it('navigates to correct cat detail URL on preview button click', () => {
cy.visit('/breeds')
cy.wait(2000)
cy.get('[data-testid="cat-breed-item"]').first().click()
cy.get('[data-testid="modal"]').should('exist').and('be.visible')

// Find the first card and extract its cat ID
cy.get('[data-testid="breed-cat-image"]')
.first()
.then($card => {
const catId = $card.attr('data-cat-id')
expect(catId, 'Cat ID should be present on card').to.exist

// Hover and click the "View" button inside it
cy.wrap($card).trigger('mouseover', { force: true })
cy.get('[data-testid="breed-cat-image"]').first().find('[data-testid="button"]').click()

// Confirm the redirection includes the correct query param
cy.location('search').should('eq', `?cat=${catId}`)
})
})
})
Loading