From 1a371abfb2d83c2bf621b6c9901665cc8744eeb0 Mon Sep 17 00:00:00 2001 From: Himanshu Bhardwaz Date: Fri, 6 Jan 2023 11:41:04 +0530 Subject: [PATCH] himanshu --- package.json | 4 +- src/components/App.tsx | 7 +++- src/components/Dog.tsx | 48 +++++++++++++++++++++++ src/components/Favourites.tsx | 52 +++++++++++++++++++++++++ src/components/SearchBox.tsx | 67 ++++++++++++++++++++++++++++++++ src/components/SearchResults.tsx | 44 +++++++++++++++++++++ src/redux/actions.ts | 35 +++++++++++++++++ src/redux/contants.ts | 6 +++ src/redux/reducer.ts | 54 ++++++++++++++++++++++++- src/redux/store.ts | 19 ++++++++- src/types/dogs-data.ts | 6 +++ yarn.lock | 18 +++++++-- 12 files changed, 351 insertions(+), 9 deletions(-) create mode 100644 src/components/Dog.tsx create mode 100644 src/components/Favourites.tsx create mode 100644 src/components/SearchBox.tsx create mode 100644 src/components/SearchResults.tsx create mode 100644 src/redux/contants.ts create mode 100644 src/types/dogs-data.ts 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"