Skip to content
This repository has been archived by the owner on Dec 14, 2023. It is now read-only.

Live challenge #43

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
150 changes: 9 additions & 141 deletions CHALLENGE.md
Original file line number Diff line number Diff line change
@@ -1,141 +1,9 @@
# Challenge

## Prerequisites

- Clone this repository to your machine.
- Do not change the repository structure. If you change it, let us know why in the README.
- Write your code in a clear and professional manner. Avoid using any sketchy or poorly written code as it will not allow us to accurately evaluate your skills.
- In case of any questions regarding the challenge, please, contact a member of the hiring team who shared this challenge with you.

## Introduction

This is a minimal Create React App SPA that includes:

- A theme for styling.
- Pre-built components for you to use in the project. You should be able to locate them.
- A fake REST API for data fetch.

![preview](.github/preview.gif)

### Notes

- The challenge _must_ be implemented using TypeScript.
- Data fetching _must_ be handled using Redux.
- Any _open-source_ library can be used, except for [Redux Toolkit](https://redux-toolkit.js.org/).
- The use of Redux Toolkit is _not_ allowed. This is to evaluate the candidate's understanding of the building blocks of Redux.
- Expected completion duration: about 3 hours.

## Tasks

### 1) Show all tournaments

#### 1.A) Show `Loading tournaments ...` while fetching:

![loading](.github/loading-state.png)

#### 1.B) Show `Something went wrong.` with a `RETRY` button when the fetching has failed:

![error](.github/error-state.png)

- Pressing the `RETRY` button will retry the fetching.

#### 1.C) Show all tournaments when the fetching has succeeded:

![success](.github/success-state.png)

#### 1.D) Show `No tournaments found.` when the fetch result is empty:

![no-result](.github/no-result-state.png)

### 2. Edit a tournament

![edit-promp](.github/edit-prompt.png)

- Pressing the `EDIT` button opens a browser prompt with the prompt message `New Tournament Name:`, an input field with the current tournament name as pre-filled value and buttons `Cancel` to cancel and `OK` to confirm.
- The tournament name must contain only Latin letters, numbers, and spaces, not an empty string or only spaces.
- When `OK` is selected, the tournament name updates immediately in the UI through an "optimistic update" with rollback, without any loading indicators.

### 3. Delete a tournament

![delete-promp](.github/delete-prompt.png)

- Pressing the `DELETE` button opens a browser prompt with the message `Do you really want to delete this tournament?` and the buttons `Cancel` to cancel and `OK` to confirm.
- When `OK` is selected, the tournament is deleted immediately in the UI using an "optimistic delete" with rollback, without any loading indicators.

### 4. Search tournaments

Requirements:

- The search function should call the endpoint with the search term, rather than searching through local data.
- The search should trigger on user input, not by pressing the Enter key.

#### 4.A) Add a search input field labeled `Search tournament ...`:

![search-input](.github/search-input.png)

#### 4.B) Display `Loading tournaments ...` while data is being fetched:

![search-loading](.github/search-loading-state.png)

#### 4.C) Display `Something went wrong.` with a `RETRY` if the data fetch fails:

![search-error](.github/search-error-state.png)

- Pressing the `RETRY` button retries the data fetch.

#### 4.D) Display all tournaments from the search results if the data fetch is successful:

![search-success](.github/search-success-state.png)

#### 4.E) Show `No tournaments found.` if the search result is empty:

![search-no-result](.github/search-no-result-state.png)

#### 4.F) Optimize the search functionality for performance.

### 5. Create a tournament

![create-tournament](.github/create-tournament.png)

- Add a button labeled `CREATE TOURNAMENT`.

![create-tournament-prompt](.github/create-tournament-prompt.png)

- Pressing the `CREATE TOURNAMENT` button opens a browser prompt with the message `Tournament Name:`, an input field to enter the tournament name, and the buttons `Cancel` to cancel and `OK` to confirm.
- The tournament name must contain only Latin letters, numbers, and spaces, not an empty string or only spaces.
- When `OK` is selected, the tournament is created on the fake REST API and at the start of the tournament list without any loading indicators.

### 6. Style it

- Tournaments have a border radius of `4px`.
- The tournament name uses the heading size `h6`.
- The `Start` date is displayed in the format `DD/MM/YYYY, HH:mm:ss` (`en-GB` locale).
- The horizontal spacing between the `EDIT` and `DELETE` buttons is `8px`.
- The horizontal and vertical spacing between each tournament is `24px`.

### 7. Make it responsive

- Examples of the expected layout on different screen sizes are provided:

- iPhone 12 Pro Max (428x926):

![iPhone 12 Pro Max](.github/iPhone-12-Pro-Max.png)

- iPad (768x1024):

![iPad](.github/iPad.png)

- Laptop 13" (1280x800):

![Laptop 13"](.github/Laptop-S.png)

- iMac Retina 27" (2560x1440):

![iMac Retina 27"](.github/iMac-Retina-27.png)

## Submission instructions

- Create a `ZIP` file with your solution.
- Include the .git folder to the `ZIP` file to facilitate code review.
- Send the `ZIP` file to the recruiter who provided this challenge.
- Do not include the `node_modules` folder in the `ZIP` file.
Ideas:

- There is a falling test, find the reason and fix it
- Given a test that validates fetch and render of list elements
- Implement fetching and rendering list of components
- Implement the test: add mock to get request
- Implement deleting mechanism for tournament
- Our players found an issue, there are two tournament created instead of one. Find a reason and fix it.
- Suggest codebase improvements
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"@types/react-dom": "^18.0.6",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/redux-mock-store": "^1.0.3",
"@types/styled-components": "^5.1.25",
"axios": "^1.4.0",
"concurrently": "^7.3.0",
"husky": "^8.0.1",
"json-server": "^0.17.0",
Expand All @@ -23,8 +25,10 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.2",
"react-router-dom": "^6.11.2",
"react-scripts": "5.0.1",
"redux": "^4.2.0",
"redux-mock-store": "^1.5.4",
"redux-thunk": "^2.4.1",
"styled-components": "^5.3.5",
"typescript": "~4.7.4",
Expand All @@ -36,7 +40,7 @@
"start:api:timeout": "json-server -p 4000 -m api/timeout.js -d -w ./api/db.js",
"start:web": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "react-scripts test --transformIgnorePatterns",
"eject": "react-scripts eject",
"prepare": "husky install"
},
Expand All @@ -62,5 +66,8 @@
"@babel/runtime": "7.18.9",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6"
},
"devDependencies": {
"axios-mock-adapter": "^1.21.5"
}
}
20 changes: 19 additions & 1 deletion src/actions/tournaments.ts
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
import { RootState } from '../reducers';
import { Tournament } from '../components/Tournament/types';

export const enum ActionType {
CREATE_TOURNAMENT = 'CREATE_TOURNAMENTS',
DELETE_TOURNAMENT = 'DELETE_TOURNAMENTS',
}

export interface CreateTournamentAction {
type: ActionType.CREATE_TOURNAMENT;
payload: Tournament;
}
export function createTournament(
tournament: Tournament
): CreateTournamentAction {
return {
type: ActionType.CREATE_TOURNAMENT,
payload: tournament,
};
}
8 changes: 8 additions & 0 deletions src/components/FormattedMessage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React, { FC } from 'react';
import H6 from '../H6';

const FormattedMessage: FC<any> = ({ children }) => {
return <H6>{children}</H6>;
};

export default FormattedMessage;
67 changes: 67 additions & 0 deletions src/components/Tournament/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { FC, useEffect, useState } from 'react';
import {
ERROR_ANIM_TIME,
TournamentActions,
TournamentDetails,
} from './styles';
import H6 from '../H6';
import Button from '../Button';
import { Tournament, TournamentConfig } from './types';
import FormattedMessage from '../FormattedMessage';

const TournamentView: FC<TournamentConfig> = ({ tournament }) => {
const [shake, setShake] = useState<boolean>(false);

useEffect(() => {
const timeoutId = setTimeout(() => {
setShake(false);
}, ERROR_ANIM_TIME);

return () => {
clearTimeout(timeoutId);
};
}, [shake]);

const delTournament = (tournament: Tournament) => {
const confirmed = window.confirm('Sure?');

if (confirmed) {
// TODO: Implement logic here
}
};

return (
<TournamentDetails doShake={shake} key={tournament.id}>
<H6>{tournament.name}</H6>
<div>
<FormattedMessage id="app.organizer" />
Organizer: {tournament.organizer}
</div>
<div>
<FormattedMessage id="app.game" />
game: {tournament.game}
</div>
<div>
<FormattedMessage id="app.participants" />
{'Participants: '}
{tournament.participants.current} /{tournament.participants.max}
</div>
<div>
<FormattedMessage id="app.start" />
Start: {tournament.startDate}
</div>
<TournamentActions>
<Button
onClick={() => {
delTournament(tournament);
}}
>
<FormattedMessage id="app.delete" />
Delete
</Button>
</TournamentActions>
</TournamentDetails>
);
};

export default TournamentView;
59 changes: 59 additions & 0 deletions src/components/Tournament/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import styled, { keyframes } from 'styled-components';
import theme from '../../theme';

export const ERROR_ANIM_TIME = 800;

interface TournamentDataProps {
doShake?: boolean;
}

const shake = keyframes`
0% {
transform: rotate(0deg) scale(1);
}
10% {
transform: rotate(-10deg) scale(1.1);
}
20% {
transform: rotate(10deg) scale(1.1);
}
30% {
transform: rotate(-10deg) scale(1.1);
}
40% {
transform: rotate(10deg) scale(1.1);
}
50% {
transform: rotate(0deg) scale(1);
}
100% {
transform: rotate(0deg) scale(1);
}
`;

export const TournamentDetails = styled.div<TournamentDataProps>`
background: ${theme.palette.background.base};
flex-basis: calc(50% - 12px);
display: flex;
flex-direction: column;
padding: 20px;
border-radius: ${theme.borderRadius};
animation: ${({ doShake }) => (doShake ? shake : 'none')} ${ERROR_ANIM_TIME}ms;
z-index: ${({ doShake }) => (doShake ? 9999 : 'auto')};
will-change: transform;

@media (min-width: ${theme.breakpoints.m}) {
flex-basis: calc(33.33% - 16px);
}

@media (max-width: ${theme.breakpoints.s}) {
flex-basis: 100%;
}
`;

export const TournamentActions = styled.div`
padding-top: 10px;
display: flex;
gap: 8px;
margin-top: auto;
`;
21 changes: 21 additions & 0 deletions src/components/Tournament/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type TournamentConfig = {
tournament: Tournament;
};
export type TournamentsState = {
data: Tournament[];
isLoading: boolean;
error: string;
guerry: string;
};
export type StoreTournaments = { [key: string]: TournamentsState };
export type Tournament = {
id: string;
name: string;
organizer: string;
game: string;
participants: {
current: number;
max: number;
};
startDate: string;
};
Loading