diff --git a/package.json b/package.json
index ed588ce..bb85957 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,8 @@
"@hot-loader/react-dom": "16.10.2",
"react-hot-loader": "^4.12.15",
"react-redux": "^7.1.1",
- "redux": "^4.0.4"
+ "redux": "^4.0.4",
+ "redux-devtools-extension": "^2.13.9",
+ "redux-thunk": "^2.4.2"
}
}
diff --git a/src/components/App.tsx b/src/components/App.tsx
index e8c7d74..12b8215 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -1,12 +1,17 @@
import React, { FC } from 'react'
import styled from '@emotion/styled'
import Header from './Header'
+import SearchBox from './SearchBox'
+import SearchResults from './SearchResults'
+import Favourites from './Favourites'
const App: FC = () => {
return (
- {/* Happy coding! */}
+
+
+
)
}
diff --git a/src/components/Dog.tsx b/src/components/Dog.tsx
new file mode 100644
index 0000000..5fec77b
--- /dev/null
+++ b/src/components/Dog.tsx
@@ -0,0 +1,48 @@
+import React from 'react'
+import styled from '@emotion/styled'
+import { setDogAsFavorites, removeDogFromFavorites } from '../redux/actions'
+import { useSelector, useDispatch } from 'react-redux'
+import Heart from './Heart'
+import { defaultRootState } from '../types/dogs-data'
+
+export default function Dog({ dog }: { dog: string }) {
+ const { favourites } = useSelector((state: defaultRootState) => state)
+ const dispatch = useDispatch()
+
+ const isDogFavourite = () => {
+ return favourites.includes(dog)
+ }
+
+ const handleFavourite = () => {
+ if (isDogFavourite()) {
+ dispatch(removeDogFromFavorites(dog))
+ } else {
+ dispatch(setDogAsFavorites(dog))
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+const DogImageContainer = styled.div({
+ position: 'relative',
+})
+
+const DogImage = styled.img({
+ width: '167.92px',
+ height: '191.58px',
+ objectFit: 'fill',
+})
+
+const HeartIconPositionContaianer = styled.div({
+ position: 'absolute',
+ bottom: '10px',
+ right: '10px',
+})
diff --git a/src/components/Favourites.tsx b/src/components/Favourites.tsx
new file mode 100644
index 0000000..bbffb22
--- /dev/null
+++ b/src/components/Favourites.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import styled from '@emotion/styled'
+import { useSelector } from 'react-redux'
+import Heart from './Heart'
+import Dog from './Dog'
+import { defaultRootState } from '../types/dogs-data'
+
+export default function Favourites() {
+ const { favourites } = useSelector((state: defaultRootState) => state)
+
+ return (
+ <>
+
+
+ Favorites
+
+ {favourites.length === 0 ? (
+
No dogs were added as favorites.
+ ) : (
+
+ {favourites?.map((imgLink) => (
+
+ ))}
+
+ )}
+ >
+ )
+}
+
+const TitleWrapper = styled.div({
+ display: 'flex',
+ marginTop: '48px',
+})
+
+const FavouriteImagesContainer = styled.div({
+ margin: '48px auto',
+ width: '100%',
+ display: 'grid',
+ gap: '30px',
+ gridTemplateColumns: 'repeat(3, 1fr)',
+})
+
+const Title = styled.h1({
+ fontWeight: 'bold',
+ fontSize: '24px',
+ lineHeight: '33px',
+ marginLeft: '20px',
+})
+
+const P = styled.div({
+ marginTop: '10px',
+})
diff --git a/src/components/SearchBox.tsx b/src/components/SearchBox.tsx
new file mode 100644
index 0000000..0db0321
--- /dev/null
+++ b/src/components/SearchBox.tsx
@@ -0,0 +1,67 @@
+import React, { FormEvent } from 'react'
+import styled from '@emotion/styled'
+import { icons } from '../assets/icons'
+import { useDispatch } from 'react-redux'
+import { fetchDogsData } from './../redux/actions'
+
+export default function SearchBox() {
+ const [breed, setBreed] = React.useState('')
+
+ const handleChange = (event: { target: { value: React.SetStateAction } }) => {
+ setBreed(event.target.value)
+ }
+
+ const dispatch = useDispatch()
+
+ const handleSearch = (event: FormEvent) => {
+ event.preventDefault()
+ if (breed) dispatch(fetchDogsData(breed))
+ }
+
+ return (
+
+
+
+
+ )
+}
+
+const SearchBoxForm = styled.form({
+ margin: '48px auto',
+ width: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: '4px',
+ border: 'none',
+})
+
+const Input = styled.input({
+ width: '100%',
+ display: 'block',
+ padding: '8px 17px',
+ background: '#F7F7F7',
+ border: 'none',
+ fontFamily: 'Nunito Sans',
+ fontStyle: 'normal',
+ fontWeight: 400,
+ fontSize: '16px',
+ lineHeight: '22px',
+ color: '#44484C',
+})
+
+const Button = styled.button({
+ alignSelf: 'stretch',
+ padding: '0 16px',
+ background: '#0794E3',
+ border: 'none',
+ color: '#FFFFFF',
+ borderRadius: '4px',
+ display: 'flex',
+ gap: '5px',
+ alignItems: 'center',
+ justifyContent: 'center',
+})
diff --git a/src/components/SearchResults.tsx b/src/components/SearchResults.tsx
new file mode 100644
index 0000000..b10f8dc
--- /dev/null
+++ b/src/components/SearchResults.tsx
@@ -0,0 +1,44 @@
+import React from 'react'
+import styled from '@emotion/styled'
+import { useSelector } from 'react-redux'
+import Dog from './Dog'
+import { defaultRootState } from '../types/dogs-data'
+
+export default function SearchResults() {
+ const { dogs, isLoading, error } = useSelector((state: defaultRootState) => state)
+
+ if (isLoading) {
+ return <>Loading...>
+ }
+
+ if (error) {
+ return <>{error}>
+ }
+
+ return (
+ <>
+ {!dogs?.message ? (
+ Please search dogs by their breed to display the results.
+ ) : (
+
+ {dogs?.message?.map((imgLink) => (
+
+ ))}
+
+ )}
+
+ >
+ )
+}
+
+const SearchResultsContainer = styled.div({
+ margin: '48px auto',
+ width: '100%',
+ display: 'grid',
+ gap: '30px',
+ gridTemplateColumns: 'repeat(3, 1fr)',
+})
+
+const Rule = styled.div({
+ border: '1px solid #DADADA',
+})
diff --git a/src/redux/actions.ts b/src/redux/actions.ts
index e69de29..be2aa52 100644
--- a/src/redux/actions.ts
+++ b/src/redux/actions.ts
@@ -0,0 +1,35 @@
+import {
+ DOGS_DATA_REQUEST,
+ DOGS_DATA_SUCCESS,
+ DOGS_DATA_FAIL,
+ SET_DOG_AS_FAVOURITE,
+ REMOVE_DOG_FROM_FAVOURITE,
+} from './contants'
+
+export const fetchDogsData = (breed: string) => async (dispatch) => {
+ try {
+ dispatch({ type: DOGS_DATA_REQUEST })
+
+ const response = await fetch(`https://dog.ceo/api/breed/${breed}/images/random/10`)
+
+ if (response.ok) {
+ const data = await response.json()
+ dispatch({ type: DOGS_DATA_SUCCESS, payload: data })
+ } else {
+ const data = await response.json()
+ if (data.code === 404) {
+ dispatch({ type: DOGS_DATA_FAIL, payload: 'Breed not found (master breed does not exist)' })
+ } else dispatch({ type: DOGS_DATA_FAIL, payload: 'Oops! something went wrong.' })
+ }
+ } catch (error) {
+ dispatch({ type: DOGS_DATA_FAIL, payload: error.message })
+ }
+}
+
+export const setDogAsFavorites = (dog: string) => (dispatch) => {
+ dispatch({ type: SET_DOG_AS_FAVOURITE, payload: dog })
+}
+
+export const removeDogFromFavorites = (dog: string) => (dispatch) => {
+ dispatch({ type: REMOVE_DOG_FROM_FAVOURITE, payload: dog })
+}
diff --git a/src/redux/contants.ts b/src/redux/contants.ts
new file mode 100644
index 0000000..bfa9cb6
--- /dev/null
+++ b/src/redux/contants.ts
@@ -0,0 +1,6 @@
+export const DOGS_DATA_REQUEST = 'DOGS_DATA_REQUEST'
+export const DOGS_DATA_SUCCESS = 'DOGS_DATA_SUCCESS'
+export const DOGS_DATA_FAIL = 'DOGS_DATA_FAIL'
+
+export const SET_DOG_AS_FAVOURITE = 'SET_DOG_AS_FAVOURITE'
+export const REMOVE_DOG_FROM_FAVOURITE = 'REMOVE_DOG_FROM_FAVOURITE'
diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts
index be51d22..2ad6060 100644
--- a/src/redux/reducer.ts
+++ b/src/redux/reducer.ts
@@ -1,5 +1,57 @@
-export const reducer = (initialState = {}, action) => {
+import {
+ DOGS_DATA_REQUEST,
+ DOGS_DATA_SUCCESS,
+ DOGS_DATA_FAIL,
+ SET_DOG_AS_FAVOURITE,
+ REMOVE_DOG_FROM_FAVOURITE,
+} from './contants'
+
+export type initialStateType = {
+ dogs: { message?: Array; status?: string }
+ isLoading: Boolean
+ error: String
+ favourites: Array
+}
+
+export const reducer = (initialState: initialStateType, action) => {
switch (action.type) {
+ case DOGS_DATA_REQUEST:
+ return {
+ ...initialState,
+ isLoading: true,
+ dogs: null,
+ error: null,
+ }
+ case DOGS_DATA_SUCCESS:
+ return {
+ ...initialState,
+ isLoading: false,
+ dogs: action.payload,
+ error: null,
+ }
+ case DOGS_DATA_FAIL:
+ return {
+ ...initialState,
+ isLoading: false,
+ dogs: null,
+ error: action.payload,
+ }
+ case SET_DOG_AS_FAVOURITE: {
+ const dog = action.payload
+
+ if (initialState.favourites.includes(dog)) return initialState
+ return {
+ ...initialState,
+ favourites: [...initialState.favourites, dog],
+ }
+ }
+ case REMOVE_DOG_FROM_FAVOURITE: {
+ const dog = action.payload
+
+ const newFavourites = initialState.favourites.filter((favourite) => favourite !== dog)
+
+ return { ...initialState, favourites: newFavourites }
+ }
default:
return initialState
}
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 06536aa..12418c6 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -1,4 +1,19 @@
-import { createStore } from 'redux'
+import { createStore, applyMiddleware } from 'redux'
+import { composeWithDevTools } from 'redux-devtools-extension'
+import thunk from 'redux-thunk'
import { reducer } from './reducer'
-export default createStore(reducer)
+const initialState = {
+ dogs: null,
+ error: null,
+ isLoading: false,
+ favourites: [],
+}
+
+const middleware = [thunk]
+
+export default createStore(
+ reducer,
+ initialState,
+ composeWithDevTools(applyMiddleware(...middleware)),
+)
diff --git a/src/types/dogs-data.ts b/src/types/dogs-data.ts
new file mode 100644
index 0000000..e8ff36b
--- /dev/null
+++ b/src/types/dogs-data.ts
@@ -0,0 +1,6 @@
+export type defaultRootState = {
+ dogs: { message?: Array; status?: string }
+ isLoading: Boolean
+ error: String
+ favourites: Array
+}
diff --git a/yarn.lock b/yarn.lock
index 2b88693..2dd895d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8659,6 +8659,16 @@ redent@^1.0.0:
indent-string "^2.1.0"
strip-indent "^1.0.1"
+redux-devtools-extension@^2.13.9:
+ version "2.13.9"
+ resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz#6b764e8028b507adcb75a1cae790f71e6be08ae7"
+ integrity sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==
+
+redux-thunk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
+ integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
+
redux@^4.0.0, redux@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
@@ -10214,10 +10224,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-typescript@^3.6.4:
- version "3.9.9"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674"
- integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==
+typescript@4.5.4:
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
+ integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
uglify-js@3.4.x:
version "3.4.10"