Add search bar
This commit is contained in:
parent
c30f5a833a
commit
525edbea07
|
@ -17,6 +17,7 @@
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"graphql": "^14.6.0",
|
"graphql": "^14.6.0",
|
||||||
"graphql-tag": "^2.10.3",
|
"graphql-tag": "^2.10.3",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
"parcel-bundler": "^1.12.4",
|
"parcel-bundler": "^1.12.4",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Component } from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
|
@ -8,6 +8,7 @@ import { Query } from 'react-apollo'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { Authorized } from './AuthorizedRoute'
|
import { Authorized } from './AuthorizedRoute'
|
||||||
import Helmet from 'react-helmet'
|
import Helmet from 'react-helmet'
|
||||||
|
import Header from './components/header/Header'
|
||||||
|
|
||||||
const adminQuery = gql`
|
const adminQuery = gql`
|
||||||
query adminQuery {
|
query adminQuery {
|
||||||
|
@ -76,21 +77,6 @@ const SideButtonLabel = styled.div`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Header = styled.div`
|
|
||||||
height: 60px;
|
|
||||||
width: 100%;
|
|
||||||
position: fixed;
|
|
||||||
background: white;
|
|
||||||
top: 0;
|
|
||||||
/* border-bottom: 1px solid rgba(0, 0, 0, 0.1); */
|
|
||||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
|
||||||
`
|
|
||||||
|
|
||||||
const Title = styled.h1`
|
|
||||||
font-size: 36px;
|
|
||||||
padding: 5px 12px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Layout = ({ children, title }) => (
|
const Layout = ({ children, title }) => (
|
||||||
<Container>
|
<Container>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -132,9 +118,7 @@ const Layout = ({ children, title }) => (
|
||||||
<div style={{ height: 24 }}></div>
|
<div style={{ height: 24 }}></div>
|
||||||
</Content>
|
</Content>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<Header>
|
<Header />
|
||||||
<Title>Photoview</Title>
|
|
||||||
</Header>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import SearchBar from './Searchbar'
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
height: 60px;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
top: 0;
|
||||||
|
/* border-bottom: 1px solid rgba(0, 0, 0, 0.1); */
|
||||||
|
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.h1`
|
||||||
|
font-size: 36px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
flex-grow: 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Header = () => (
|
||||||
|
<Container>
|
||||||
|
<Title>Photoview</Title>
|
||||||
|
<SearchBar />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Header
|
|
@ -0,0 +1,200 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { useLazyQuery } from '@apollo/react-hooks'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import debounce from 'lodash/debounce'
|
||||||
|
import ProtectedImage from '../photoGallery/ProtectedImage'
|
||||||
|
import { NavLink } from 'react-router-dom'
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
height: 60px;
|
||||||
|
width: 350px;
|
||||||
|
margin: 0 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SearchField = styled.input`
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 4px #eee;
|
||||||
|
border-color: #3d82c6;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Results = styled.div`
|
||||||
|
display: ${({ show }) => (show ? 'block' : 'none')};
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
max-height: calc(100vh - 100px);
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 28px 4px 32px;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 0 4px #eee;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
top: 50%;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
${SearchField}:not(:focus) ~ & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SEARCH_QUERY = gql`
|
||||||
|
query searchQuery($query: String!) {
|
||||||
|
search(query: $query) {
|
||||||
|
albums {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
thumbnail {
|
||||||
|
thumbnail {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
photos {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
thumbnail {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SearchBar = () => {
|
||||||
|
const [fetchSearches, fetchResult] = useLazyQuery(SEARCH_QUERY)
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [fetched, setFetched] = useState(false)
|
||||||
|
|
||||||
|
let debouncedFetch = null
|
||||||
|
const fetchEvent = e => {
|
||||||
|
e.persist()
|
||||||
|
|
||||||
|
if (!debouncedFetch) {
|
||||||
|
debouncedFetch = debounce(() => {
|
||||||
|
console.log('searching', e.target.value.trim())
|
||||||
|
fetchSearches({ variables: { query: e.target.value.trim() } })
|
||||||
|
setFetched(true)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuery(e.target.value)
|
||||||
|
if (e.target.value.trim() != '') {
|
||||||
|
debouncedFetch()
|
||||||
|
} else {
|
||||||
|
setFetched(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = null
|
||||||
|
if (query.trim().length > 0 && fetched) {
|
||||||
|
results = <SearchResults result={fetchResult} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<SearchField type="search" placeholder="Search" onChange={fetchEvent} />
|
||||||
|
{results}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResultTitle = styled.h1`
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 12px 0 4px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SearchResults = ({ result }) => {
|
||||||
|
const { data, loading } = result
|
||||||
|
|
||||||
|
const photos = (data && data.search.photos) || []
|
||||||
|
const albums = (data && data.search.albums) || []
|
||||||
|
|
||||||
|
let message = null
|
||||||
|
if (loading) message = 'Loading results...'
|
||||||
|
else if (data && photos.length == 0 && albums.length == 0)
|
||||||
|
message = 'No results found'
|
||||||
|
|
||||||
|
const albumElements = albums.map(album => (
|
||||||
|
<AlbumRow key={album.id} {...album} />
|
||||||
|
))
|
||||||
|
|
||||||
|
const photoElements = photos.map(photo => (
|
||||||
|
<PhotoRow key={photo.id} {...photo} />
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Results
|
||||||
|
onMouseDown={e => {
|
||||||
|
// Prevent input blur event
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
show={data}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
<ResultTitle>Albums</ResultTitle>
|
||||||
|
{albumElements}
|
||||||
|
<ResultTitle>Photos</ResultTitle>
|
||||||
|
{photoElements}
|
||||||
|
</Results>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const RowLink = styled(NavLink)`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
color: black;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PhotoSearchThumbnail = styled(ProtectedImage)`
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: contain;
|
||||||
|
`
|
||||||
|
|
||||||
|
const AlbumSearchThumbnail = styled(ProtectedImage)`
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
object-fit: cover;
|
||||||
|
`
|
||||||
|
|
||||||
|
const RowTitle = styled.span`
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PhotoRow = photo => (
|
||||||
|
<RowLink to="/">
|
||||||
|
<PhotoSearchThumbnail src={photo.thumbnail.url} />
|
||||||
|
<RowTitle>{photo.title}</RowTitle>
|
||||||
|
</RowLink>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AlbumRow = album => (
|
||||||
|
<RowLink to={`/album/${album.id}`}>
|
||||||
|
<AlbumSearchThumbnail src={album.thumbnail.thumbnail.url} />
|
||||||
|
<RowTitle>{album.title}</RowTitle>
|
||||||
|
</RowLink>
|
||||||
|
)
|
||||||
|
|
||||||
|
SearchResults.propTypes = {
|
||||||
|
result: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchBar
|
Loading…
Reference in New Issue