diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7d8811 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# These are some examples of commonly ignored file patterns. +# You should customize this list as applicable to your project. +# Learn more about .gitignore: +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv + +.vercel + +README.md \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..e472c9f --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "gcreacttest", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "dependencies": { + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^28.1.6", + "@types/node": "^18.0.6", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.6", + "axios": "^0.27.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "5.3.3", + "react-router-dom": "5.3.3", + "react-scripts": "5.0.1", + "sass": "^1.53.0", + "typescript": "^4.7.4", + "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@types/react-router-dom": "^5.3.3", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "prettier": "^2.7.1" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest", + "plugin:prettier/recommended" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/public/Images/huma.png b/public/Images/huma.png new file mode 100755 index 0000000..510b2c8 Binary files /dev/null and b/public/Images/huma.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..c04fb68 --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + Gain Changer + + + +
+ + + diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/public/logo512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/public/manifest.json @@ -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" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..4ad0818 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,84 @@ +import Navbar from 'components/Navbar' +import ScrollToTop from 'components/ScrollToTop' +import Sidebar from 'components/Sidebar' +import ErrorHandler from 'ErrorHandler' +import Countries from 'pages/Countries/Countries' +import Dashboard from 'pages/Dashboard/Dashboard' +import React, { useEffect, useRef, useState } from 'react' +import { + BrowserRouter as Router, + Route, + Switch, + Redirect, + useLocation, +} from 'react-router-dom' + +const App = () => { + return ( + + + + + + ) +} + +const Root = () => { + let location = useLocation() + const [showSidebar, setShowSidebar] = useState(false) + const sidebarRef = useRef(null) + + window.addEventListener('mousedown', hideSidebar) + + function hideSidebar(event: MouseEvent) { + const target = event.target as HTMLElement + if (sidebarRef.current && sidebarRef.current.contains(target)) { + return '' + } + setShowSidebar(false) + } + + useEffect(() => { + if (showSidebar) { + setShowSidebar(false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.pathname]) + + useEffect(() => { + return () => { + window.removeEventListener('mousedown', hideSidebar) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + function toggleSidebar(): void { + setShowSidebar(!showSidebar) + } + return ( +
+
+
+ +
+ +
+ +
+ + + + + + +
+
+
+
+ ) +} + +export default App diff --git a/src/ErrorHandler.tsx b/src/ErrorHandler.tsx new file mode 100644 index 0000000..ecec2e2 --- /dev/null +++ b/src/ErrorHandler.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' + +type Props = { + children: React.ReactNode +} + +type State = { + error?: Error +} + +export default class ErrorHandler extends React.Component { + state = { + error: undefined, + } + + componentDidCatch(error: Error) { + this.setState({ error }) + } + + render() { + return this.state.error ?
Error?
: this.props.children + } +} diff --git a/src/assests/fonts/BR-Firma-Bold.woff b/src/assests/fonts/BR-Firma-Bold.woff new file mode 100755 index 0000000..52fd29d Binary files /dev/null and b/src/assests/fonts/BR-Firma-Bold.woff differ diff --git a/src/assests/fonts/BR-Firma-Light.woff b/src/assests/fonts/BR-Firma-Light.woff new file mode 100755 index 0000000..e250aa6 Binary files /dev/null and b/src/assests/fonts/BR-Firma-Light.woff differ diff --git a/src/assests/fonts/BR-Firma-Regular.woff b/src/assests/fonts/BR-Firma-Regular.woff new file mode 100755 index 0000000..8703da2 Binary files /dev/null and b/src/assests/fonts/BR-Firma-Regular.woff differ diff --git a/src/assests/fonts/BR-Firma-Thin.woff b/src/assests/fonts/BR-Firma-Thin.woff new file mode 100755 index 0000000..51e885e Binary files /dev/null and b/src/assests/fonts/BR-Firma-Thin.woff differ diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..800503b --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import '../styles/components/navbar.scss' + +type Props = { + toggleSidebar: () => void +} + +const Navbar = ({ toggleSidebar }: Props) => { + return ( +
+
+
+
+
+
+
+

Gain Temp

+
+
+
+ profile +
+
+
+ ) +} + +export default Navbar diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx new file mode 100644 index 0000000..eab8521 --- /dev/null +++ b/src/components/ScrollToTop.tsx @@ -0,0 +1,14 @@ +import { useEffect } from 'react' +import { useLocation } from 'react-router-dom' + +const ScrollToTop = () => { + const { pathname } = useLocation() + + useEffect(() => { + window.scrollTo(0, 0) + }, [pathname]) + + return null +} + +export default ScrollToTop diff --git a/src/components/Select.tsx b/src/components/Select.tsx new file mode 100644 index 0000000..98ed7fe --- /dev/null +++ b/src/components/Select.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import '../styles/components/select.scss' + +type Props = { + name: string + value: string + onChange: (event: React.ChangeEvent) => void + options: Array<{ name: string; value: number | string }> + disabled?: boolean + width?: string +} + +const Select = ({ name, value, disabled, onChange, options, width }: Props) => { + return ( +
+ +
+ ) +} + +export default Select diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..db49d27 --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { NavLink } from 'react-router-dom' +import '../styles/components/sidebar.scss' + +const Sidebar = () => { + return ( +
+ +
+ ) +} + +export default Sidebar diff --git a/src/components/StatsCard.tsx b/src/components/StatsCard.tsx new file mode 100644 index 0000000..173e1ed --- /dev/null +++ b/src/components/StatsCard.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import '../styles/components/statscard.scss' + +type Props = { + showLoader: boolean + summary: Array<{ name: string; value: number | string }> +} + +const StatsCard = ({ showLoader, summary }: Props) => { + return ( +
+ {summary.map((item, i) => ( +
+
{item.name}
+ {showLoader ? ( + + ) : ( +
{item.value || 0}
+ )} +
+ ))} +
+ ) +} + +export default StatsCard diff --git a/src/components/Table.tsx b/src/components/Table.tsx new file mode 100644 index 0000000..4f75d41 --- /dev/null +++ b/src/components/Table.tsx @@ -0,0 +1,50 @@ +import '../styles/components/table.scss' + +type TableT = { + name: string + index: string + id: number +} + +type Props = { + columns: TableT[] + data: Array<{ [key: string]: string }> + loading: string +} + +const Table = ({ columns, data, loading }: Props) => { + const showLoading = loading && loading === 'loading' + return ( +
+ + + + {columns && + columns.map(column => )} + + {data && + data.map(item => ( + + {columns && + columns.map(column => ( + + ))} + + ))} + +
{column.name}
{item[column.index]}
+ {data.length === 0 && showLoading ? ( +
+ +
+ ) : null} + {data && data.length === 0 && !showLoading && ( +
+

No Data

+
+ )} +
+ ) +} + +export default Table diff --git a/src/context/app-context.tsx b/src/context/app-context.tsx new file mode 100644 index 0000000..d02924d --- /dev/null +++ b/src/context/app-context.tsx @@ -0,0 +1,82 @@ +import axios from 'axios' +import { createContext, useContext, useEffect, useState } from 'react' + +export type CountryProps = { + name: string + latlng: string + lat: string + lon: string + population: string + timezones: string + continents: string +} + +interface ContextState { + countries: CountryProps[] | [] + status: string +} + +const AppContext = createContext({} as ContextState) + +type Props = { + children: React.ReactNode +} + +const AppProvider = ({ children }: Props) => { + const [countries, setCountries] = useState([]) + const [status, setStatus] = useState('loading') + + useEffect(() => { + getCountries() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const getCountries = async (): Promise => { + interface ResponseType { + name: { common: string } + latlng: string + population: string + timezones: string + continents: string + } + setStatus('loading') + try { + const response = await axios.get(`https://restcountries.com/v3.1/all`) + let responseData = response.data + let countries: CountryProps[] = [] + + responseData.forEach((item: ResponseType) => { + countries.push({ + name: item.name.common, + latlng: item.latlng.toString(), + lat: item.latlng[0], + lon: item.latlng[1], + population: item.population, + timezones: item.timezones[0], + continents: item.continents[0], + }) + }) + + setCountries(countries) + setStatus('success') + } catch (error) { + setStatus('fail') + } + } + + return ( + + {children} + + ) +} + +const useAppData = (): ContextState => { + const appData = useContext(AppContext) + if (appData === undefined) { + throw new Error('useAppData must be used within a AppProvider') + } + return appData +} + +export { useAppData, AppProvider } diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..5b657a5 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,20 @@ +import 'styles/index.scss' +import App from './App' +import React from 'react' +import ReactDOM from 'react-dom/client' +import reportWebVitals from './reportWebVitals' +import { AppProvider } from 'context/app-context' + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) +root.render( + + + + + , +) + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals() diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/Countries/Countries.tsx b/src/pages/Countries/Countries.tsx new file mode 100644 index 0000000..3cda190 --- /dev/null +++ b/src/pages/Countries/Countries.tsx @@ -0,0 +1,22 @@ +import Table from 'components/Table' +import { useAppData } from 'context/app-context' + +export const tableColumns = [ + { name: 'Name', index: 'name', id: 1 }, + { name: 'Population', index: 'population', id: 2 }, + { name: 'Timezones', index: 'timezones', id: 3 }, + { name: 'Continents', index: 'continents', id: 4 }, + { name: 'Longitude', index: 'lon', id: 5 }, + { name: 'Latitude', index: 'lat', id: 6 }, +] +const Countries = () => { + const { countries, status } = useAppData() + + return ( +
+ + + ) +} + +export default Countries diff --git a/src/pages/Dashboard/Dashboard.tsx b/src/pages/Dashboard/Dashboard.tsx new file mode 100644 index 0000000..d1746e4 --- /dev/null +++ b/src/pages/Dashboard/Dashboard.tsx @@ -0,0 +1,115 @@ +import axios from 'axios' +import Select from 'components/Select' +import StatsCard from 'components/StatsCard' +import { useAppData } from 'context/app-context' +import React, { useEffect, useMemo, useState } from 'react' +import '../../styles/pages/dashboard.scss' + +const API_KEY = process.env.REACT_APP_API_KEY + +type WeatherT = { + temp_min: string + temp_max: string + humidity: string + speed: string + deg: string + pressure: string +} + +type CountryT = { + name: string + value: string +} + +type CountryValue = CountryT['value'] + +const Dashboard = () => { + const { countries, status: appLoading } = useAppData() + + const [status, setStatus] = useState('loading') + const [weatherStats, setWeatherStats] = useState({} as WeatherT) + const [country, setCountry] = useState('35,139') + + useEffect(() => { + getWeatherData() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [country]) + + const getWeatherData = async (): Promise => { + setStatus('loading') + let lat: string = country.split(',')[0] + let lon: string = country.split(',')[1] + let url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}` + + try { + const response = await axios.get(url) + const { main, wind } = response.data + + const responseData = { ...main, ...wind } + + setWeatherStats(responseData) + setStatus('success') + } catch (error) { + setStatus('fail') + } + } + + function getCountryList(): CountryT[] { + let countryList: CountryT[] = [{ name: 'Select Countries', value: '' }] + countries.forEach(item => { + countryList.push({ name: item.name, value: item.latlng }) + }) + return countryList + } + + function handleCountrySelect(event: React.ChangeEvent) { + const target = event.target as HTMLSelectElement + setCountry(target.value) + } + + const countryPositon = useMemo( + () => getCountryList(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [countries], + ) + + return ( +
+ +
+
+

Welcome back

+

+ Here's what has been happening in the last days +

+
+ +
+