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
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"arrowParens": "avoid",
"semi": false,
"tabWidth": 4
}
41 changes: 18 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
# In name of Allah
## How to set up:

## Introduction
> :warning: If you get **API rate limit exceded** error: Add your own API Key in .env file (Step 2) )!

We are going to develop a react based web application to get live scores of football games. It will be a simple panel (**only mobile design is needed**) with an **infinite tab** scrolling side to side. Each page indicates a specific day with list of games played on that day. The list of games will be categorized based on their league. You could get a grasp of features in the image below. Ofcourse this is not a 0 or 1 task, so you can push code based on how much progress you have made, but try to implement features as similar as you can (it has points in final review).
### 1) Make sure you have node >=18.0.0 installed.

![Screenshot from 2022-06-25 17-02-51](https://user-images.githubusercontent.com/61571233/175773756-b6e136dc-4ca7-4ee2-b88e-248d4591a638.png "UI for live score page")
<p align="center">
UI for live score page
</p>
### 2) Create a `.env` and add following variables (not necessary, I provided a key in "api-key.helper.ts"):

**Note**: For data, use [this api documentation](https://www.api-football.com/documentation-v3). You should create an account in panel & get the free api-key to use. The fixtures part in this documentation provides necessary data for this task, also note that this api has a daily limit.
```
VITE_API_KEY='Your api key'
```

### To install the dependencies, run:

## Expectations
```
yarn
```

So What does matter to us?
- a clean structure of codebase & components
- clean code practices
- ability to layout page correctly
- well designed API calls
- profound understanding of states
- finally, ability to learn
### To run a development server, run:

## Tasks
```
yarn dev
```

1. Fork this repository
2. Estimate the develop
3. Learn & Develop
4. Push your code to your repository
5. Send us a pull request, we will review and get back to you
6. Enjoy
## Links

- [Live website](https://react-footballi.netlify.app)
16 changes: 16 additions & 0 deletions custom.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
declare module '*.svg' {
import * as React from 'react'

export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>

const src: string
export default src
}

declare module '*.jpg'
declare module '*.jpeg'
declare module '*.png'
declare module '*.woff'
declare module '*.woff2'
13 changes: 13 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Footballli Challenge | Ali Farajzade</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
38 changes: 38 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "react-footballli",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^4.0.10",
"@tanstack/react-query-devtools": "^4.0.10",
"axios": "^0.27.2",
"daisyui": "^2.20.0",
"date-fns": "^2.29.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
"react-loading-skeleton": "^3.1.0",
"react-router-dom": "^6.3.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/uuid": "^8.3.4",
"@vitejs/plugin-react": "^2.0.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.14",
"tailwindcss": "^3.1.6",
"typescript": "^4.6.4",
"vite": "^3.0.0"
},
"engines": {
"node": "18.0.0"
}
}
6 changes: 6 additions & 0 deletions postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file added public/images/2.webp
Binary file not shown.
Binary file added public/images/3.webp
Binary file not shown.
Binary file added public/images/router.webp
Binary file not shown.
1 change: 1 addition & 0 deletions public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { formatISO } from 'date-fns'
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import Information from './components/information.component'
import Layout from './components/layout.component'
import Phone from './components/phone.component'
import useMediaQuery from './hooks/use-media-query.hook'

const App: React.FC = () => {
const navigate = useNavigate()

const matches = useMediaQuery('(min-width: 600px)')

useEffect(() => {
navigate(
`/results/${formatISO(new Date(), { representation: 'date' })}`
)
}, [])

return <Layout>{matches ? <Information /> : <Phone />}</Layout>
}
export default App
18 changes: 18 additions & 0 deletions src/api/config.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from 'axios'
import { API_KEY } from '../helpers/api-key.helper'
import { TMatch } from '../types/match.types'

const axiosInstance = axios.create({
baseURL: 'https://v3.football.api-sports.io',
headers: { 'x-apisports-key': API_KEY },
})

export const getMatchInfo = async (
date: string,
status: 'NS' | 'FT'
): Promise<TMatch[]> =>
(
await axiosInstance.get(
`/fixtures?date=${date}&timezone=Asia/Tehran&status=${status}`
)
).data.response
24 changes: 24 additions & 0 deletions src/components/error.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { BiError } from 'react-icons/bi'

interface IProps {
message?: string
containerClassName?: string
iconClassName?: string
messageClassName?: string
}

const ErrorComponent: React.FC<IProps> = ({
message = 'Something went wrong',
containerClassName,
iconClassName,
messageClassName,
}) => {
return (
<div className={containerClassName}>
<BiError className={iconClassName} />
<p className={messageClassName}>{message}</p>
</div>
)
}

export default ErrorComponent
9 changes: 9 additions & 0 deletions src/components/header-container.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
interface IProps {
children: React.ReactNode[] | React.ReactNode
}

const HeaderContainer: React.FC<IProps> = ({ children }) => {
return <div className="w-full pt-8 bg-white">{children}</div>
}

export default HeaderContainer
12 changes: 12 additions & 0 deletions src/components/header.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AiFillClockCircle } from 'react-icons/ai'

const Header: React.FC = () => {
return (
<div className="flex items-center justify-between p-4 w-full text-neutral">
<h1 className="text-base font-bold">Live Results</h1>
<AiFillClockCircle className="text-lg" />
</div>
)
}

export default Header
116 changes: 116 additions & 0 deletions src/components/information.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const Information: React.FC = () => {
return (
<div className="text-center select-none">
<h1 className="text-blue-400 font-bold text-2xl mb-5">
React Footballi Challenge
</h1>
<h2 className="text-base mb-5">
⚠️If you get <strong>API rate limit exceded</strong> error,
Change the API Key in .env file.⚠️ More info on{' '}
<a
className="link link-accent"
href="https://github.com/AliFarajzade/React-Footballli#readme"
target="_blank"
>
README.md
</a>
</h2>
<div className="flex items-center flex-wrap justify-center gap-5 mb-20">
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Typescript_logo_2020.svg/768px-Typescript_logo_2020.svg.png?20210506173343"
alt="TypeScript"
className="w-12 h-12 object-contain"
title="TypeScript"
/>
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/768px-React-icon.svg.png?20220125121207"
alt="React"
className="w-12 h-12 object-contain"
title="React"
/>
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Tailwind_CSS_Logo.svg/900px-Tailwind_CSS_Logo.svg.png?20211001194333"
alt="TailwindCSS"
className="w-12 h-12 object-contain"
title="TailwindCSS"
/>
<img
src="https://seeklogo.com/images/R/react-query-logo-1340EA4CE9-seeklogo.com.png"
alt="React Query"
className="w-12 h-12 object-contain"
title="React Query"
/>
<img
src="https://user-images.githubusercontent.com/8939680/57233882-20344080-6fe5-11e9-9086-d20a955bed59.png"
alt="Axios"
className="w-12 h-12 object-contain"
title="Axios"
/>
<img
src="/images/router.webp"
alt="React Router"
className="w-12 h-12 object-contain"
title="React Router"
/>
<img
src="https://cdn.worldvectorlogo.com/logos/vitejs.svg"
alt="Vite"
className="w-12 h-12 object-contain"
title="Vite"
/>
</div>
<div className="flex items-baseline justify-between flex-wrap gap-20">
<div className="flex flex-col items-center gap-8">
<span className="flex justify-center items-center text-lg p-7 w-6 h-6 rounded-full bg-slate-800 text-white">
1
</span>
<div className="flex flex-col items-center gap-2">
<span className="text-base font-bold">Press</span>{' '}
<div>
<kbd className="kbd">ctrl</kbd>+
<kbd className="kbd">shift</kbd>+
<kbd className="kbd">i</kbd>
</div>
</div>
</div>
<div className="flex flex-col items-center gap-8">
<span className="flex justify-center items-center text-lg p-7 w-6 h-6 rounded-full bg-slate-800 text-white">
2
</span>
<div className="flex flex-col items-center gap-2">
<span className="text-base font-bold">
Toggle Device Toolbar
</span>{' '}
<div>
<img
className="w-72 h-72 object-contain"
src="/images/2.webp"
alt="Step 2"
/>
</div>
</div>
</div>
<div className="flex flex-col items-center gap-8">
<span className="flex justify-center items-center text-lg p-7 w-6 h-6 rounded-full bg-slate-800 text-white">
3
</span>
<div className="flex flex-col items-center gap-2">
<span className="text-base font-bold">
Select A Phone Dimension
</span>{' '}
<div>
<img
className="w-72 h-72 object-contain"
src="/images/3.webp"
alt="Step 3"
/>
</div>
</div>
</div>
</div>
</div>
)
}

export default Information

12 changes: 12 additions & 0 deletions src/components/layout.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
interface IProps {
children: React.ReactNode[] | React.ReactNode
}
const Layout: React.FC<IProps> = ({ children }) => {
return (
<div className="min-h-screen w-full flex items-center justify-center bg-gray-200">
{children}
</div>
)
}

export default Layout
31 changes: 31 additions & 0 deletions src/components/match-info-card.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { format } from 'date-fns'
import { TMatch, TResult } from '../types/match.types'

interface IProps {
result: TMatch
}

const MatchInfoCard: React.FC<IProps> = ({ result }) => {
return (
<div className="bg-white w-full p-4 px-3 rounded-md shadow-md flex items-center justify-between min-h-[80px] relative">
{Object.values(result.teams).map((resultObj: TResult) => (
<div className="flex items-center gap-2">
<img
className="mask mask-circle object-contain object-center w-7 h-7"
src={resultObj.logo}
/>
<span className="text-xs font-bold text-gray-700 max-w-[50px] text-center">
{resultObj.name.substring(0, 16)}
</span>
</div>
))}
<span className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
{result.goals.home !== null && result.goals.away !== null
? `${result.goals.home} - ${result.goals.away}`
: format(new Date(result.fixture.date), 'p')}
</span>
</div>
)
}

export default MatchInfoCard
Loading