This is a simple one page React Application that pull restaurant data from a single API, displays all the data in the table and allows users to search restaurants by either name, state or genre. All filters and search can be applied separately or combined together.
- A user should be able to see a table with the name, city, state, phone number, and genres for each restaurant.
- A user should see results sorted by name in alphabetical order starting with the beginning of the alphabet
- A user should be able to filter restaurants by state.
- A user should be able to filter by genre.
- State and Genre filters should default to “All” and take effect instantaneously (no additional clicks).
- A user should be able to enter text into a search field. When hitting the enter key or clicking on a search button, the table should search results. Search results should match either the name, city, or genre.
- A user should be able to clear the search by clearing the text value in the search input.
- A user should only see 10 results at a time and the table should be paginated.
- A user should be able to combine filters and search. The user should be able to turn filters on and off while a search value is present.
- If any of the filters do not return any restaurants, the UI should indicate that no results were found.
- CI/CD
- Unit Testing ( +some integration testing )
- A user can click table row to see additional information
- A user can also filter by attire and combine attire filter with other filters and search
- Add more integration tests, test button click, search, filters...
- Display mini-map when more information is showed
- Add filter by tags functionality and combine it with other filters
- Ability to use filters by clicking on tag or genre directly
- Add some media queries, make the app fully responsive
- Utilize PWA
- Refactor combining filters functionality or move it to separate components
- Implement user authentication
Used GitHub Projects to track progress of development https://github.com/edignot/Restaurants/projects/1
I used multiple array prototypes e.g. reduce, map, forEach... to convert data as needed. Also many other ES6 features like object restructuring or arrow functions.
I always choose React together with React Hooks, that allows me to use only functional components.
I prefer Redux, because it makes application more scalable in the future if needed. Redux setup is relatively easy and low time consuming, especially when utilizing Redux Hooks. I have used Redux Thunk to make it easier to dispatch action that makes a network request.
I prefer using Axios rather that Fetch, because Axios transforms JSON data automatically, so syntax is a bit cleaner.
Deployed on Netlify https://restaurantssearchengine.netlify.app/ and automatically builds after master updates
I ussually add unit as well as integration tests for each project I'm building. This time I mostly unit tested and only added a few integrations tests, because of short timeframe. I'll keep working on the tests in the future
My initial idea was to use SCSS ( I'm a big fan of it ), utilize SCSS mixins and other functionalities. But due to short timeframe given for this code challenge I decided to style my app with plain CSS. I was still able to use CSS variables for colors and plan to add more varianbles for constants or switch to SCSS in the future.
...
it('should have a type FILTER_RESTAURANTS', () => {
const action = {
type: 'FILTER_RESTAURANTS',
filteredRestaurants,
}
const result = actions.filterRestaurants(filteredRestaurants)
expect(result).toEqual(action)
})
...
it('should have a type SET_STATE_FILTER', () => {
const action = {
type: 'SET_STATE_FILTER',
stateFilter,
}
const result = actions.setStateFilter(stateFilter)
expect(result).toEqual(action)
})
...
...
it('should return session state with updated genre filter', () => {
const action = {
type: 'SET_GENRE_FILTER',
genreFilter: sessionTestData.genreFilter,
}
const result = session(sessionState, action)
expect(result).toEqual({
...sessionState,
genreFilter: sessionTestData.genreFilter,
})
})
...
...
describe('Restaurant', () => {
let RestaurantComponent, store, mockStore, initialState, restaurant
beforeEach(() => {
initialState = initialStateTestData
restaurant = initialStateTestData.restaurants[0]
mockStore = configureStore()
store = mockStore(initialState)
RestaurantComponent = render(
<Provider store={store}>
<Restaurant restaurant={restaurant} />
</Provider>,
)
})
it('Restaurant Component should successfully render', () => {
const { getByText } = RestaurantComponent
expect(getByText(restaurant.name)).toBeInTheDocument()
expect(
getByText(`${restaurant.city} , ${restaurant.state}`),
).toBeInTheDocument()
expect(getByText(`+1 ${restaurant.telephone}`)).toBeInTheDocument()
for (const genre of restaurant.genreArray) {
expect(getByText(genre)).toBeInTheDocument()
}
expect(
getByText(
`${restaurant.address1} ${restaurant.city} ${restaurant.state} ${restaurant.zip}`,
),
).toBeInTheDocument()
expect(getByText(restaurant.hours)).toBeInTheDocument()
expect(getByText(restaurant.website)).toBeInTheDocument()
for (const tag of restaurant.tagsArray) {
expect(getByText(`# ${tag}`)).toBeInTheDocument()
}
expect(getByText(restaurant.attire)).toBeInTheDocument()
})
})
import React from 'react'
import Restaurants from '../../containers/Restaurants/Restaurants'
import Form from '../../containers/Form/Form'
import './RestaurantsPage.css'
const RestaurantsPage = () => {
return (
<section className='restaurants-page-container'>
<Form />
<Restaurants />
</section>
)
}
export default RestaurantsPage
...
const searchHandler = () => {
const filteredRestaurants = restaurants.reduce((filtered, restaurant) => {
if (
restaurant.name.toUpperCase().includes(searchValue.toUpperCase()) ||
restaurant.city.toUpperCase().includes(searchValue.toUpperCase()) ||
restaurant.genre.toUpperCase().includes(searchValue.toUpperCase())
) {
filtered.push(restaurant.id)
}
return filtered
}, [])
dispatch(filterRestaurants(filteredRestaurants))
}
...
...
const Pagination = ({ restaurantsPerPage, totalRestaurants, paginate }) => {
const pageNumbers = []
for (let i = 1; i <= Math.ceil(totalRestaurants / restaurantsPerPage); i++) {
pageNumbers.push(i)
}
return (
<ul className='pages-wrapper'>
{pageNumbers.map((number) => (
<li
key={number}
className='page-item'
onClick={() => {
paginate(number)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
>
<p>{number}</p>
</li>
))}
</ul>
)
}
...
...
const Restaurants = () => {
const dispatch = useDispatch()
const restaurants = useSelector((store) => store.restaurants)
const session = useSelector((store) => store.session)
const [loading, setLoading] = useState(false)
const [restaurantsPerPage] = useState(10)
useEffect(() => {
const fetchRestaurants = async () => {
setLoading(true)
await dispatch(getRestaurants())
setLoading(false)
}
fetchRestaurants()
}, [dispatch])
...
...
const Dropdown = ({ possibleOptions, title, type }) => {
const dispatch = useDispatch()
const handleSelectorChange = (e) => {
e.target.value === '' &&
type === 'genre' &&
dispatch(setGenreFilter(e.target.value))
e.target.value === '' &&
type === 'state' &&
dispatch(setStateFilter(e.target.value))
e.target.value === '' &&
type === 'attire' &&
dispatch(setAttireFilter(e.target.value))
type === 'genre' && dispatch(setGenreFilter(e.target.value))
type === 'attire' && dispatch(setAttireFilter(e.target.value))
type === 'state' && dispatch(setStateFilter(e.target.value))
dispatch(setCurrentPageNumber(1))
e.target.value = ''
}
...
import React from 'react'
import PropTypes from 'prop-types'
import Header from '../../components/Header/Header'
import Footer from '../../components/Footer/Footer'
const Layout = ({ children }) => {
return (
<section>
<Header />
{children}
<Footer />
</section>
)
}
export default Layout
Layout.propTypes = {
children: PropTypes.node,
}
const genres = genreArray.map((genre) => (
<li key={uid()} className='genres-item'>
{genre}
</li>
))
import axios from 'axios'
const URL = 'https://code-challenge.spectrumtoolbox.com/api/restaurants'
const API_KEY = `Api-Key ${process.env.REACT_APP_API_KEY}`
export const fetchRestaurants = () =>
axios.get(URL, {
headers: {
Authorization: API_KEY,
},
})
export default (restaurants = [], action) => {
switch (action.type) {
case 'FETCH_RESTAURANTS':
return action.payload
default:
return restaurants
}
}
-Restaurants Actions with Redux Thunk file
import * as api from '../api'
export const getRestaurants = () => async (dispatch) => {
try {
const { data } = await api.fetchRestaurants()
const restaurants = data.map((restaurant) => {
const genreArray = restaurant.genre.split(',')
const tagsArray = restaurant.tags.split(',')
return {
...restaurant,
attire: restaurant.attire.toLowerCase(),
genreArray,
tagsArray,
}
})
const restaurantsSortedByName = restaurants.sort((a, b) =>
a.name > b.name ? 1 : b.name > a.name ? -1 : 0,
)
dispatch({
type: 'FETCH_RESTAURANTS',
payload: restaurantsSortedByName,
})
} catch (error) {
console.log(error)
}
}
...
export const clearAll = () => ({
type: 'CLEAR_ALL',
})
export const setCurrentPageNumber = (currentPageNumber) => ({
type: 'SET_CURRENT_PAGE_NUMBER',
currentPageNumber,
})
export const setGenreFilter = (genreFilter) => ({
type: 'SET_GENRE_FILTER',
genreFilter,
})
...
- clone repo
- run
npm i
- create .env file and add
REACT_APP_API_KEY=<change this to valid API key>
- run
npm start
- open
http://localhost:3000