diff --git a/src/components/App.tsx b/src/components/App.tsx index e8c7d74..ed5fb25 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,12 +1,75 @@ import React, { FC } from 'react' import styled from '@emotion/styled' import Header from './Header' +import SearchBar from './SearchBar' +import ImageBox from './ImageBox' +import { useDispatch, useSelector } from 'react-redux' +import { StateProps } from '../redux/reducer' +import Favorite from './Favorite' +import { setDogData } from '../redux/actions' +import { Dispatch } from 'redux' +import Spinner from './Spinner' +import NoDogImage from './NoDogImage' const App: FC = () => { + const { favorites, dogs } = useSelector((state: StateProps) => state) + const dispatch: Dispatch = useDispatch() + + const getDogs = async (breed: string) => { + dispatch(setDogData({ breed, data: [], status: 'loading' })) + try { + const res = await fetch(`https://dog.ceo/api/breed/${breed}/images`) + if (!res.ok) throw new Error() + const data = await res.json() + const imgArr = data.message.slice(1, 11) + const imgData = imgArr.map((el, i) => { + return { + id: i + 1, + url: el, + } + }) + return dispatch(setDogData({ breed, data: imgData, status: 'completed' })) + } catch (error) { + dispatch(setDogData({ breed: dogs.breed, data: [], status: 'rejected' })) + } + } + + React.useEffect(() => { + getDogs(dogs.breed) + }, [dogs.breed]) + + console.log(dogs) + return (
{/* Happy coding! */} + + {dogs.status === 'loading' ? ( +
+ +
+ ) : ( + <> + {dogs.status === 'rejected' ? ( + + ) : ( + <> +

Showing images of {dogs.breed} dog

+ + {dogs.data && dogs.data.map((dog) => )} + + + )} + + )} + {favorites.length > 0 && } ) } @@ -18,4 +81,14 @@ const Container = styled.div({ paddingTop: '60px', }) +const ImagesContainer = styled.div({ + display: 'flex', + flexWrap: 'wrap', + gap: '30px', + justifyContent: 'space-between', + paddingBottom: '30px', + marginBottom: '20px', + borderBottom: '2px solid #ccc', +}) + export default App diff --git a/src/components/Favorite.tsx b/src/components/Favorite.tsx new file mode 100644 index 0000000..c14ca41 --- /dev/null +++ b/src/components/Favorite.tsx @@ -0,0 +1,45 @@ +import React, { FC } from 'react' +import { useSelector } from 'react-redux' +import { StateProps } from '../redux/reducer' + +import Heart from './Heart' +import ImageBox from './ImageBox' +import styled from '@emotion/styled' + +const Favorite: FC = () => { + const dogs = useSelector((state: StateProps) => state.favorites) + return ( +
+ + + Favorites + + + {dogs.map((dog) => ( + + ))} + +
+ ) +} + +const TitleDiv = styled.div({ + display: 'flex', + alignItems: 'center', +}) + +const Title = styled.h2({ + fontWeight: 'bold', + fontSize: '24px', + lineHeight: '33px', + marginLeft: '40px', +}) + +const ImagesContainer = styled.div({ + display: 'flex', + flexWrap: 'wrap', + gap: '16px', + justifyContent: 'flex-start', +}) + +export default Favorite diff --git a/src/components/ImageBox.tsx b/src/components/ImageBox.tsx new file mode 100644 index 0000000..4abe265 --- /dev/null +++ b/src/components/ImageBox.tsx @@ -0,0 +1,58 @@ +import React, { FC } from 'react' +import styled from '@emotion/styled' +import Heart from './Heart' +import { Dispatch } from 'redux' +import { useDispatch, useSelector } from 'react-redux' +import { addToFavorite, removeFromFavorite } from '../redux/actions' +import { StateProps } from '../redux/reducer' + +const ImageBox: FC<{ dog: { id: number; url: string } }> = ({ dog }) => { + const { favorites } = useSelector((state: StateProps) => state) + const dispatch: Dispatch = useDispatch() + + const isImageInFavorites = favorites.find((el) => el.id === dog.id) + + const handelClick = () => { + if (isImageInFavorites) { + return dispatch(removeFromFavorite(dog)) + } else dispatch(addToFavorite(dog)) + } + + return ( + + dog image + + + + + ) +} + +const ImageContainer = styled.div({ + cursor: 'pointer', + width: '160px', + height: '160px', + position: 'relative', + borderRadius: '10px', + overflow: 'hidden', + transition: 'all 0.3s ease-in-out', + + ':hover': { + boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)', + transform: 'translateY(-10px)', + }, +}) + +const Image = styled.img({ + width: '100%', + height: '100%', + objectFit: 'cover', +}) + +const HeartBtn = styled.div({ + position: 'absolute', + bottom: '10px', + right: '10px', +}) + +export default ImageBox diff --git a/src/components/NoDogImage.tsx b/src/components/NoDogImage.tsx new file mode 100644 index 0000000..061c651 --- /dev/null +++ b/src/components/NoDogImage.tsx @@ -0,0 +1,31 @@ +import styled from '@emotion/styled' +import React, { FC } from 'react' + +const NoDogImage: FC<{ message: string }> = ({ message }) => { + return ( +
+ {message} + Try again +
+ ) +} + +const Div = styled.div({ + minHeight: '80vh', + display: 'grid', + placeContent: 'center', +}) + +const Message = styled.h2({ + fontSize: '24px', + lineHeight: '20px' +}) + +const TryPara = styled.p({ + textAlign: 'center', + TextDecoder: 'underline', + color: 'red', + fontWeight: 'bold', +}) + +export default NoDogImage diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx new file mode 100644 index 0000000..8850c45 --- /dev/null +++ b/src/components/SearchBar.tsx @@ -0,0 +1,68 @@ +import React, { FC } from 'react' +import styled from '@emotion/styled' +import { icons } from '../assets' +import { useDispatch} from 'react-redux' +import { Dispatch } from 'redux' +import { setDogBreed } from '../redux/actions' + +const SearchBar :FC= ()=>{ + const [searchTerm , setSearchTerm] = React.useState('') + const dispatch:Dispatch = useDispatch() + + const handelChange = (e:React.ChangeEvent) =>{ + return setSearchTerm(e.target.value) + } + const handelClick = ()=>{ + dispatch(setDogBreed(searchTerm)) + return setSearchTerm('') + } + + return ( + + + + + ) +} + + +const FlexContainer = styled.div({ + display: 'flex', + alignItems: 'center', + marginTop: '48px', + marginBottom: '32px', +}) + + +const SearchInput = styled.input({ + width: '80%', + font: 'inherit', + padding: '10px', + border: '1px solid #ccc', + borderRightWidth: 0, + borderRadius: '8px 0 0 8px', + outline: 'none', +}) + +const Button = styled.button({ + cursor: 'pointer', + width: '20%', + padding: '10px', + font: 'inherit', + backgroundColor: '#0794E3', + color: 'white', + display: 'flex', + alignItems: 'center', + border: '1px solid #ccc', + borderRadius: '0 8px 8px 0', + outline: 'none', +}) + +const SearchIcon = styled.img({ + width: '17px', + height: '15px', + alignSelf: 'center', + marginRight: '10px', +}) + +export default SearchBar \ No newline at end of file diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx new file mode 100644 index 0000000..278eeba --- /dev/null +++ b/src/components/Spinner.tsx @@ -0,0 +1,29 @@ +import React from 'react' + +import { keyframes } from '@emotion/core' +import styled from '@emotion/styled' + +const Spinner = () => { + return +} + +const rotate = keyframes` + 0% { + transform: rotate(0); + } + 100% { + transform: rotate(360deg); + } +` + +const SpinnerDiv = styled.div({ + width: '64px', + height: '64px', + border: '5px solid #ccc', + borderBottomColor: 'transparent', + borderTopColor: 'transparent', + borderRadius: '50%', + animation: `${rotate} 1s linear infinite`, +}) + +export default Spinner diff --git a/src/redux/actions.ts b/src/redux/actions.ts index e69de29..af39a39 100644 --- a/src/redux/actions.ts +++ b/src/redux/actions.ts @@ -0,0 +1,24 @@ +import { DogsProps } from "./reducer"; + +export enum ACTION_TYPE { + 'SetDogsData' = 'SetDogsData', + 'AddToFavorite' = 'AddToFavorite', + 'RemoveFromFavorite' = 'RemoveFromFavorite', + 'SetDogBreed' = 'SetDogBreed', +} + +export function addToFavorite(payload: { id: number; url: string }) { + return { type: ACTION_TYPE.AddToFavorite, payload } +} + +export function removeFromFavorite(payload: { id: number; url: string }) { + return { type: ACTION_TYPE.RemoveFromFavorite, payload } +} + +export function setDogBreed(payload: string) { + return { type: ACTION_TYPE.SetDogBreed, payload } +} + +export function setDogData(payload: DogsProps) { + return { type: ACTION_TYPE.SetDogsData, payload } +} diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index be51d22..dea7de5 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -1,6 +1,46 @@ -export const reducer = (initialState = {}, action) => { +import { ACTION_TYPE } from './actions' + +export type DogDataProps = { id: number; url: string }[] +export type StatusProps = 'loading' | 'rejected' | 'completed' | undefined +export type DogsProps = { + breed: string + data: DogDataProps + status: StatusProps +} + +export type StateProps = { + favorites: { id: number; url: string }[] + dogs: DogsProps +} + +const initialState: StateProps = { + favorites: [], + dogs: { breed: 'frise', data: [], status: undefined }, +} + +export const reducer = (state = initialState, action) => { switch (action.type) { + case ACTION_TYPE.SetDogBreed: + return { + ...state, + dogs: { ...state.dogs, breed: action.payload }, + } + case ACTION_TYPE.SetDogsData: + return { + ...state, + dogs: action.payload, + } + case ACTION_TYPE.AddToFavorite: + return { + ...state, + favorites: [...state.favorites, action.payload], + } + case ACTION_TYPE.RemoveFromFavorite: + return { + ...state, + favorites: state.favorites.filter((dog) => dog.id !== action.payload.id), + } default: - return initialState + return state } }