Skip to content

Commit

Permalink
add recoil to project
Browse files Browse the repository at this point in the history
  • Loading branch information
Michal Bartosik committed Jun 4, 2021
1 parent e3a3994 commit 70c096d
Show file tree
Hide file tree
Showing 23 changed files with 14,425 additions and 271 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
"prettier": "^2.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",
"react-icons": "^4.2.0",
"react-media-hook": "^0.4.9",
"react-paginate": "^7.1.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-spinners": "^0.10.6",
"recoil": "^0.3.1",
"styled-components": "^5.3.0",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1"
Expand Down
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300;600;800&display=swap"
rel="stylesheet"
/>
<title>REST Countries API with color theme switcher</title>
<title>Countries of the World</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
52 changes: 0 additions & 52 deletions src/App.test.tsx

This file was deleted.

88 changes: 23 additions & 65 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,45 @@
import { Suspense, useEffect } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { useSetRecoilState, useRecoilValue } from 'recoil';

import { GlobalStyles } from 'styles/GlobalStyles';
import { useState, useEffect } from 'react';
import Header from 'components/Header/Header';
import getCountries from './helpers/api';
import Country from 'models/Country';
import { GlobalStyles } from 'styles/GlobalStyles';
import { Wrapper } from './App.styles';
import Home from 'pages/Home/Home';
import CountryDetails from 'pages/CountryDetails/CountryDetails';
import useDarkMode from 'hooks/UseDarkMode';
import Regions from 'enums/Regions';
import Spinner from 'components/Spinner';
import { regionFilterState, searchQueryState, currentPageState } from 'store';
import ErrorFallback from 'components/ErrorFallback';

function App() {
const [countries, setCountries] = useState<Country[]>([]);
const [searchQuery, setSearchQuery] = useState<string>('');
const [selectedRegion, setSelectedRegion] = useState<string>(Regions.All);
const [filteredCountries, setFilteredCountries] = useState<Country[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const filter = useRecoilValue(regionFilterState);
const searchQuery = useRecoilValue(searchQueryState);
const setCurrentPage = useSetRecoilState(currentPageState);
const [theme, themeToggler] = useDarkMode();
const [currentPage, setCurrentPage] = useState(0);

useEffect(() => {
(async () => {
setIsLoading(true);
try {
const data = await getCountries();
setCountries(data);
setIsError(false);
} catch (e) {
setIsError(true);
} finally {
setIsLoading(false);
}
})();
}, []);

const handleSelectedRegion = (target: any): void => {
setSelectedRegion(target.selectedItem);
};

useEffect(() => {
if (selectedRegion === Regions.All) {
setFilteredCountries(countries);
} else {
setFilteredCountries(
countries.filter(country => country.region === selectedRegion)
);
}
}, [countries, selectedRegion]);

useEffect(() => {
setCurrentPage(0);
}, [selectedRegion, searchQuery]);

const handlePageClick = ({ selected }: any): void => {
setCurrentPage(selected);
window.scrollTo(0, 0);
};
}, [filter, searchQuery, setCurrentPage]);

return (
<Router>
<Wrapper>
<GlobalStyles />
<Header toggleTheme={themeToggler} theme={theme} />
<Switch>
<Route exact path="/">
<Home
searchQuery={searchQuery}
selectedRegion={selectedRegion}
setSearchQuery={setSearchQuery}
filteredCountries={filteredCountries}
handleSelectedRegion={handleSelectedRegion}
isLoading={isLoading}
isError={isError}
currentPage={currentPage}
handlePageClick={handlePageClick}
/>
</Route>
<Route path="/:code">
<CountryDetails countries={countries} />
</Route>
</Switch>
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Spinner />}>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/:code">
<CountryDetails />
</Route>
</Switch>
</Suspense>
</ErrorBoundary>
</Wrapper>
</Router>
);
Expand Down
35 changes: 35 additions & 0 deletions src/components/CountriesList/CountriesList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { render, screen } from 'test-utils';
import userEvent from '@testing-library/user-event';
import * as api from 'helpers/api';
import data from 'mocks/data';
import Country from 'models/Country';
import CountriesList from 'components/CountriesList/CountriesList';

jest.mock('../../helpers/api.ts');
let mockGetCountries: jest.SpyInstance<Promise<Country[]>>;

beforeEach(() => {
mockGetCountries = jest.spyOn(api, 'getCountries');
});

describe('App component', () => {
it('renders countries list if request succeeds', async () => {
mockGetCountries.mockImplementationOnce((): Promise<any> => {
return Promise.resolve(data);
});

render(<CountriesList />);
const countries = await screen.findAllByTestId('country');
expect(mockGetCountries).toBeCalledTimes(1);
expect(countries).toHaveLength(20);
});

it('renders error message if request does not succeed', async () => {
mockGetCountries.mockImplementationOnce((): Promise<any> => {
return Promise.reject();
});
render(<CountriesList />);
const errorMessage = await screen.findByText(/something went wrong/i);
expect(errorMessage).toBeInTheDocument();
});
});
71 changes: 42 additions & 29 deletions src/components/CountriesList/CountriesList.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,60 @@
import React from 'react';
import { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import ReactPaginate from 'react-paginate';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';

import ClipLoader from 'react-spinners/ClipLoader';
import { css } from '@emotion/core';
import { Wrapper, Notification } from './CountriesList.styles';
import CountryCard from 'components/CountryCard/CountryCard';
import Country from 'models/Country';
import Pagination from 'enums/Pagination';
import { filteredCountriesState, currentPageState } from 'store';

const CountriesList: React.FC<{
countries: Country[];
selectedRegion: string;
isLoading: boolean;
isError: boolean;
}> = ({ countries, isLoading, isError }) => {
const override = css`
display: block;
margin: 3rem auto;
`;

if (!countries.length && !isLoading && !isError) {
const CountriesList: React.FC = () => {
const filteredCountries = useRecoilValue(filteredCountriesState);
const [currentPage, setCurrentPage] = useRecoilState(currentPageState);

useEffect(() => {
window.scrollTo(0, 0);
}, [currentPage]);

if (!filteredCountries.length) {
return <Notification>No results found</Notification>;
}

if (isError) {
return (
<Notification>
Something went wrong. Please try to refresh the page.
</Notification>
);
}
const offset = currentPage * Pagination.PageSize;
const currentPageData: Country[] = filteredCountries.slice(
offset,
offset + Pagination.PageSize
);
const pageCount = Math.ceil(filteredCountries.length / Pagination.PageSize);
const shouldPaginationBeShown =
filteredCountries.length > Pagination.PageSize;

const handlePageClick = ({ selected }: any): void => {
setCurrentPage(selected);
};

return (
<>
<ClipLoader
css={override}
color="#36D7B7"
loading={isLoading}
size={60}
></ClipLoader>
<Wrapper>
{countries.map((country: Country) => {
{currentPageData.map((country: Country) => {
return <CountryCard key={country.alpha3Code} {...country} />;
})}
</Wrapper>
{shouldPaginationBeShown && (
<ReactPaginate
previousLabel={<FaChevronLeft />}
nextLabel={<FaChevronRight />}
breakLabel={'...'}
pageCount={pageCount}
marginPagesDisplayed={Pagination.MarginPagesDisplayed}
pageRangeDisplayed={Pagination.PageRangeDisplayed}
onPageChange={handlePageClick}
activeClassName={'active'}
containerClassName={'pagination'}
forcePage={currentPage}
/>
)}
</>
);
};
Expand Down
23 changes: 14 additions & 9 deletions src/components/DropDownSelect/DropDownSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import { render, screen } from '@testing-library/react';
import { render, screen } from 'test-utils';
import userEvent from '@testing-library/user-event';
import DropDownSelect from './DropDownSelect';
import Regions from 'enums/Regions';

let mockHandleSelectedItem: jest.Mock<any, any>;
let selectedRegion: string;

beforeEach(() => {
selectedRegion = 'All';
mockHandleSelectedItem = jest.fn();
render(
<DropDownSelect
selectedRegion={selectedRegion}
handleSelectedItem={mockHandleSelectedItem}
/>
);
render(<DropDownSelect />);
});

describe('DropDownSelect component', () => {
Expand Down Expand Up @@ -44,4 +37,16 @@ describe('DropDownSelect component', () => {
userEvent.click(selectedOption);
expect(selectedOption).toHaveAttribute('aria-selected', 'true');
});

it('displays correct option after selecting from dropdown', () => {
const indexOfEurope = 4;
const button = screen.getByText('All');

userEvent.click(button);
const options = screen.getAllByRole('option');
const selectedOption = options[indexOfEurope];
userEvent.click(selectedOption);

expect(button).toHaveTextContent('Europe');
});
});
Loading

1 comment on commit 70c096d

@vercel
Copy link

@vercel vercel bot commented on 70c096d Jun 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.