Skip to content
This repository was archived by the owner on Mar 17, 2025. It is now read-only.

Added features to search and favorite #73

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { FC } from 'react'
import styled from '@emotion/styled'
import Header from './Header'
import AppLayout from './AppLayout'

const App: FC = () => {
return (
<Container>
<Header />
{/* Happy coding! */}
{/* Happy coding! (Ty) */}
<AppLayout />
</Container>
)
}
Expand All @@ -16,6 +18,10 @@ const Container = styled.div({
height: '100%',
width: '560px',
paddingTop: '60px',
'@media only screen and (max-width: 600px)': {
padding: '40px',
width: '100%',
},
})

export default App
23 changes: 23 additions & 0 deletions src/components/AppLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'

import Main from './search/Main'
import Aside from './favorites/Aside'

import styled from '@emotion/styled'

function AppLayout() {
return (
<>
<Main />
<Separator />
<Aside />
</>
)
}

const Separator = styled.hr({
border: 'none',
borderTop: '0.2rem solid #f7f7f7',
})

export default AppLayout
25 changes: 25 additions & 0 deletions src/components/favorites/Aside.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useSelector } from 'react-redux'

import FavoritesHeader from './FavoritesHeader'
import Favorited from './Favorited'
import MessageDisplay from '../ui/MessageDisplay'

import styled from '@emotion/styled'

function Aside() {
const { favoriteDogs } = useSelector((store) => store.favorites)

return (
<AsideLayout>
<FavoritesHeader />
{favoriteDogs.length > 0 && <Favorited />}
{favoriteDogs.length === 0 && <MessageDisplay>No favorites yet 🐕</MessageDisplay>}
</AsideLayout>
)
}

const AsideLayout = styled.aside({
paddingBottom: '2rem',
})

export default Aside
34 changes: 34 additions & 0 deletions src/components/favorites/Favorited.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useSelector, useDispatch } from 'react-redux'

import GridList from '../ui/GridList'
import ImageItem from '../ui/ImageItem'
import InteractiveFloatingHeart from '../ui/InteractiveFloatingHeart'

import { toggleFavorite } from '../../redux/actions'

const favoritedGridStyled = {
gridTemplateColumns: 'repeat(4, minmax(22%,1fr))',
gridTemplateRows: '1fr',
gap: '1rem',
margin: '1rem 0',
'@media only screen and (max-width: 600px)': {
gap: '0.4rem',
},
}

function Favorited() {
const { favoriteDogs } = useSelector((store) => store.favorites)
const dispatch = useDispatch()

return (
<GridList customStyles={favoritedGridStyled}>
{favoriteDogs.map((dog) => (
<ImageItem src={dog.imageURL} key={dog.id}>
<InteractiveFloatingHeart isFilled onClick={() => dispatch(toggleFavorite(dog))} />
</ImageItem>
))}
</GridList>
)
}

export default Favorited
25 changes: 25 additions & 0 deletions src/components/favorites/FavoritesHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Heart from '../Heart'

import styled from '@emotion/styled'

function FavoritesHeader() {
return (
<HeaderLayout>
<Heart icon="redHeartIcon" alt="Your Favourites" />
<Heading>Favorites</Heading>
</HeaderLayout>
)
}

const HeaderLayout = styled.div({
display: 'flex',
gap: '1rem',
})

const Heading = styled.h2({
fontWeight: 'bold',
fontSize: '1.25rem',
lineHeight: '1.5rem',
})

export default FavoritesHeader
22 changes: 22 additions & 0 deletions src/components/search/Main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react'
import { useSelector } from 'react-redux'

import Search from './Search'
import Results from './Results'
import MessageDisplay from './../ui/MessageDisplay'

function Main() {
const { searchedDogs, error } = useSelector((store) => store.search)

return (
<main>
<Search />
{searchedDogs.length > 0 && <Results />}
{searchedDogs.length === 0 && (
<MessageDisplay>{error ? error : 'Start searching dogs today 🐶'}</MessageDisplay>
)}
</main>
)
}

export default Main
37 changes: 37 additions & 0 deletions src/components/search/Results.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useDispatch, useSelector } from 'react-redux'

import InteractiveFloatingHeart from '../ui/InteractiveFloatingHeart'
import GridList from '../ui/GridList'
import ImageItem from '../ui/ImageItem'

import { toggleFavorite } from '../../redux/actions'

const resultGridStyled = {
gridTemplateColumns: 'repeat(3, minmax(28%,1fr))',
gridTemplateRows: 'repeat(4, 1fr)',
margin: '2rem 0',
}

function Results() {
const { searchedDogs } = useSelector((store) => store.search)
const { favoriteDogs } = useSelector((store) => store.favorites)
const dispatch = useDispatch()

return (
<GridList customStyles={resultGridStyled}>
{searchedDogs.map((dog) => {
const isFavorite = favoriteDogs.find((d) => d.id === dog.id)
return (
<ImageItem src={dog.imageURL} key={dog.id}>
<InteractiveFloatingHeart
isFilled={isFavorite}
onClick={() => dispatch(toggleFavorite(dog))}
/>
</ImageItem>
)
})}
</GridList>
)
}

export default Results
99 changes: 99 additions & 0 deletions src/components/search/Search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'

import { icons } from '../../assets'
import { setError, setLoading, setSearchResults } from './../../redux/actions.ts'

import 'regenerator-runtime/runtime'
import styled from '@emotion/styled'

function Search() {
const [localQuery, setLocalQuery] = useState('')

const { isLoading } = useSelector((store) => store.search)
const dispatch = useDispatch()

async function handleFetchResults(e) {
e.preventDefault()

if (!localQuery) return
const query = localQuery.toLowerCase()
dispatch(setLoading())
try {
const response = await fetch(`https://dog.ceo/api/breed/${query}/images/random/10`)
const data = await response.json()
if (data.status === 'error') throw new Error(`Failed to find ${localQuery} dogs 😔`)
dispatch(setSearchResults(data, query))
} catch (err) {
dispatch(setError(err.message))
}
}

return (
<SearchLayout onSubmit={handleFetchResults}>
<Input type="text" value={localQuery} onChange={(e) => setLocalQuery(e.target.value)} />
<Button type="button" onClick={handleFetchResults} disabled={isLoading}>
<SearchIcon src={icons['searchIcon']} alt="search" />
<span>{isLoading ? 'Loading' : 'Search'}</span>
</Button>
</SearchLayout>
)
}

const SearchLayout = styled.form({
backgroundColor: '#f7f7f7',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginTop: '2rem',
borderRadius: '0.2rem',
overflow: 'hidden',
'&:focus-within': {
boxShadow: 'inset 0 0 0 0.1rem rgb(138,138,138, 0.2)',
},
})

const Input = styled.input({
backgroundColor: 'transparent',
border: 'none',
width: '100%',
fontFamily: 'inherit',
fontSize: 'inherit',
color: '#8a8a8a',
margin: '0.2rem 1rem',
'&:focus': {
outline: 'none',
},
})

const Button = styled.button({
backgroundColor: '#0794e3',
alignSelf: 'stretch',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.4rem',
fontFamily: 'inherit',
fontSize: 'inherit',
color: '#fff',
border: 'none',
borderRadius: '0.2rem',
padding: '0.2rem 0.8rem',
transition: 'all 0.3s ease-out',
'&:hover': {
cursor: 'pointer',
backgroundColor: '#20afff',
},
'&:disabled': {
cursor: 'not-allowed',
backgroundColor: '#20afff',
},
})

const SearchIcon = styled.img({
height: '1rem',
width: '1rem',
})

export default Search
23 changes: 23 additions & 0 deletions src/components/ui/GridList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import styled from '@emotion/styled'

function GridList({ children, customStyles }) {
return <Grid customStyles={customStyles}>{children}</Grid>
}

const Grid = styled.ul((props) => ({
listStyle: 'none',
display: 'grid',
gridTemplateColumns: 'repeat(3, minmax(min-content,max-content))',
gridTemplateRows: '1fr',
gap: '2rem',
height: '100%',
width: '100%',
padding: '0',
margin: '0',
...props.customStyles,
'@media only screen and (max-width: 600px)': {
gap: '1rem',
},
}))

export default GridList
30 changes: 30 additions & 0 deletions src/components/ui/ImageItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import styled from '@emotion/styled'

function ImageItem({ src, children }) {
return (
<Item>
<Image src={src} />
{children}
</Item>
)
}

const Item = styled.li({
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
aspectRatio: '1 / 1',
position: 'relative',
borderRadius: '0.3rem',
overflow: 'hidden',
})

const Image = styled.img({
objectFit: 'cover',
height: '100%',
width: '100%',
})

export default ImageItem
40 changes: 40 additions & 0 deletions src/components/ui/InteractiveFloatingHeart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { icons } from './../../assets'

import styled from '@emotion/styled'

function InteractiveFloatingHeart({ customStyles, isFilled = false, onClick = () => {} }) {
return (
<HeartLayout customStyles={customStyles}>
<InteractiveHeart
src={isFilled ? icons['redHeartIcon'] : icons['whiteHeartIcon']}
isFilled={isFilled}
alt="love this doggo"
onClick={onClick}
/>
</HeartLayout>
)
}

const InteractiveHeart = styled.img(({ isFilled }) => ({
transition: 'all 0.3s ease-out',
'&:hover': {
willChange: 'transform',
transform: 'scale(1.2)',
filter: isFilled
? 'saturate(0.8) brightness(3.8)'
: 'invert(0.19) saturate(15) brightness(0.9) sepia(2) contrast(0.89) hue-rotate(-30deg)',
cursor: 'pointer',
},
}))

const HeartLayout = styled.div(({ customStyles }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
bottom: '0.4rem',
right: '0.4rem',
...customStyles,
}))

export default InteractiveFloatingHeart
Loading