1
Fork 0

Merge pull request #14 from viktorstrate/search

Implement search
This commit is contained in:
Viktor Strate Kløvedal 2020-03-05 20:36:42 +01:00 committed by GitHub
commit 225f22bcfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 658 additions and 42 deletions

View File

@ -140,6 +140,7 @@ type ComplexityRoot struct {
MyPhotos func(childComplexity int, filter *models.Filter) int MyPhotos func(childComplexity int, filter *models.Filter) int
MyUser func(childComplexity int) int MyUser func(childComplexity int) int
Photo func(childComplexity int, id int) int Photo func(childComplexity int, id int) int
Search func(childComplexity int, query string, limitPhotos *int, limitAlbums *int) int
ShareToken func(childComplexity int, token string, password *string) int ShareToken func(childComplexity int, token string, password *string) int
SiteInfo func(childComplexity int) int SiteInfo func(childComplexity int) int
User func(childComplexity int, filter *models.Filter) int User func(childComplexity int, filter *models.Filter) int
@ -152,6 +153,12 @@ type ComplexityRoot struct {
Success func(childComplexity int) int Success func(childComplexity int) int
} }
SearchResult struct {
Albums func(childComplexity int) int
Photos func(childComplexity int) int
Query func(childComplexity int) int
}
ShareToken struct { ShareToken struct {
Album func(childComplexity int) int Album func(childComplexity int) int
Expire func(childComplexity int) int Expire func(childComplexity int) int
@ -216,6 +223,7 @@ type QueryResolver interface {
MyPhotos(ctx context.Context, filter *models.Filter) ([]*models.Photo, error) MyPhotos(ctx context.Context, filter *models.Filter) ([]*models.Photo, error)
Photo(ctx context.Context, id int) (*models.Photo, error) Photo(ctx context.Context, id int) (*models.Photo, error)
ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error) ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error)
Search(ctx context.Context, query string, limitPhotos *int, limitAlbums *int) (*models.SearchResult, error)
} }
type ShareTokenResolver interface { type ShareTokenResolver interface {
Owner(ctx context.Context, obj *models.ShareToken) (*models.User, error) Owner(ctx context.Context, obj *models.ShareToken) (*models.User, error)
@ -770,6 +778,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.Photo(childComplexity, args["id"].(int)), true return e.complexity.Query.Photo(childComplexity, args["id"].(int)), true
case "Query.search":
if e.complexity.Query.Search == nil {
break
}
args, err := ec.field_Query_search_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.Search(childComplexity, args["query"].(string), args["limitPhotos"].(*int), args["limitAlbums"].(*int)), true
case "Query.shareToken": case "Query.shareToken":
if e.complexity.Query.ShareToken == nil { if e.complexity.Query.ShareToken == nil {
break break
@ -829,6 +849,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ScannerResult.Success(childComplexity), true return e.complexity.ScannerResult.Success(childComplexity), true
case "SearchResult.albums":
if e.complexity.SearchResult.Albums == nil {
break
}
return e.complexity.SearchResult.Albums(childComplexity), true
case "SearchResult.photos":
if e.complexity.SearchResult.Photos == nil {
break
}
return e.complexity.SearchResult.Photos(childComplexity), true
case "SearchResult.query":
if e.complexity.SearchResult.Query == nil {
break
}
return e.complexity.SearchResult.Query(childComplexity), true
case "ShareToken.album": case "ShareToken.album":
if e.complexity.ShareToken.Album == nil { if e.complexity.ShareToken.Album == nil {
break break
@ -1045,6 +1086,8 @@ type Query {
photo(id: Int!): Photo! photo(id: Int!): Photo!
shareToken(token: String!, password: String): ShareToken! shareToken(token: String!, password: String): ShareToken!
search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
} }
type Mutation { type Mutation {
@ -1234,6 +1277,12 @@ type PhotoEXIF {
"An index describing the mode for adjusting the exposure of the image" "An index describing the mode for adjusting the exposure of the image"
exposureProgram: Int exposureProgram: Int
} }
type SearchResult {
query: String!
albums: [Album!]!
photos: [Photo!]!
}
`}, `},
) )
@ -1623,6 +1672,36 @@ func (ec *executionContext) field_Query_photo_args(ctx context.Context, rawArgs
return args, nil return args, nil
} }
func (ec *executionContext) field_Query_search_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 string
if tmp, ok := rawArgs["query"]; ok {
arg0, err = ec.unmarshalNString2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["query"] = arg0
var arg1 *int
if tmp, ok := rawArgs["limitPhotos"]; ok {
arg1, err = ec.unmarshalOInt2ᚖint(ctx, tmp)
if err != nil {
return nil, err
}
}
args["limitPhotos"] = arg1
var arg2 *int
if tmp, ok := rawArgs["limitAlbums"]; ok {
arg2, err = ec.unmarshalOInt2ᚖint(ctx, tmp)
if err != nil {
return nil, err
}
}
args["limitAlbums"] = arg2
return args, nil
}
func (ec *executionContext) field_Query_shareToken_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Query_shareToken_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -4325,6 +4404,50 @@ func (ec *executionContext) _Query_shareToken(ctx context.Context, field graphql
return ec.marshalNShareToken2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) return ec.marshalNShareToken2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
} }
func (ec *executionContext) _Query_search(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Query",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithResolverContext(ctx, rctx)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Query_search_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
rctx.Args = args
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().Search(rctx, args["query"].(string), args["limitPhotos"].(*int), args["limitAlbums"].(*int))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*models.SearchResult)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNSearchResult2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSearchResult(ctx, field.Selections, res)
}
func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field) ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() { defer func() {
@ -4542,6 +4665,117 @@ func (ec *executionContext) _ScannerResult_message(ctx context.Context, field gr
return ec.marshalOString2ᚖstring(ctx, field.Selections, res) return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
} }
func (ec *executionContext) _SearchResult_query(ctx context.Context, field graphql.CollectedField, obj *models.SearchResult) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "SearchResult",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Query, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _SearchResult_albums(ctx context.Context, field graphql.CollectedField, obj *models.SearchResult) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "SearchResult",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Albums, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]*models.Album)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNAlbum2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbumᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _SearchResult_photos(ctx context.Context, field graphql.CollectedField, obj *models.SearchResult) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "SearchResult",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Photos, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]*models.Photo)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNPhoto2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _ShareToken_id(ctx context.Context, field graphql.CollectedField, obj *models.ShareToken) (ret graphql.Marshaler) { func (ec *executionContext) _ShareToken_id(ctx context.Context, field graphql.CollectedField, obj *models.ShareToken) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field) ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() { defer func() {
@ -6836,6 +7070,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
} }
return res return res
}) })
case "search":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_search(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "__type": case "__type":
out.Values[i] = ec._Query___type(ctx, field) out.Values[i] = ec._Query___type(ctx, field)
case "__schema": case "__schema":
@ -6887,6 +7135,43 @@ func (ec *executionContext) _ScannerResult(ctx context.Context, sel ast.Selectio
return out return out
} }
var searchResultImplementors = []string{"SearchResult"}
func (ec *executionContext) _SearchResult(ctx context.Context, sel ast.SelectionSet, obj *models.SearchResult) graphql.Marshaler {
fields := graphql.CollectFields(ec.RequestContext, sel, searchResultImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("SearchResult")
case "query":
out.Values[i] = ec._SearchResult_query(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "albums":
out.Values[i] = ec._SearchResult_albums(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "photos":
out.Values[i] = ec._SearchResult_photos(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var shareTokenImplementors = []string{"ShareToken"} var shareTokenImplementors = []string{"ShareToken"}
func (ec *executionContext) _ShareToken(ctx context.Context, sel ast.SelectionSet, obj *models.ShareToken) graphql.Marshaler { func (ec *executionContext) _ShareToken(ctx context.Context, sel ast.SelectionSet, obj *models.ShareToken) graphql.Marshaler {
@ -7537,6 +7822,20 @@ func (ec *executionContext) marshalNScannerResult2ᚖgithubᚗcomᚋviktorstrate
return ec._ScannerResult(ctx, sel, v) return ec._ScannerResult(ctx, sel, v)
} }
func (ec *executionContext) marshalNSearchResult2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSearchResult(ctx context.Context, sel ast.SelectionSet, v models.SearchResult) graphql.Marshaler {
return ec._SearchResult(ctx, sel, &v)
}
func (ec *executionContext) marshalNSearchResult2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐSearchResult(ctx context.Context, sel ast.SelectionSet, v *models.SearchResult) graphql.Marshaler {
if v == nil {
if !ec.HasError(graphql.GetResolverContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._SearchResult(ctx, sel, v)
}
func (ec *executionContext) marshalNShareToken2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx context.Context, sel ast.SelectionSet, v models.ShareToken) graphql.Marshaler { func (ec *executionContext) marshalNShareToken2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx context.Context, sel ast.SelectionSet, v models.ShareToken) graphql.Marshaler {
return ec._ShareToken(ctx, sel, &v) return ec._ShareToken(ctx, sel, &v)
} }

View File

@ -47,6 +47,12 @@ type ScannerResult struct {
Message *string `json:"message"` Message *string `json:"message"`
} }
type SearchResult struct {
Query string `json:"query"`
Albums []*Album `json:"albums"`
Photos []*Photo `json:"photos"`
}
// General public information about the site // General public information about the site
type SiteInfo struct { type SiteInfo struct {
InitialSetup bool `json:"initialSetup"` InitialSetup bool `json:"initialSetup"`

View File

@ -152,30 +152,9 @@ func (r *photoResolver) Thumbnail(ctx context.Context, obj *models.Photo) (*mode
return url, nil return url, nil
} }
// func processPhoto(db *sql.DB, photo *models.Photo) error {
// tx, err := db.Begin()
// if err != nil {
// return err
// }
// err = scanner.ProcessPhoto(tx, photo)
// if err != nil {
// tx.Rollback()
// log.Printf("ERROR: Could not process photo: %s\n", err)
// return errors.New(fmt.Sprintf("Could not process photo: %s\n", err))
// }
// err = tx.Commit()
// if err != nil {
// log.Printf("ERROR: Could not commit photo after process to db: %s\n", err)
// return err
// }
// return nil
// }
func (r *photoResolver) Album(ctx context.Context, obj *models.Photo) (*models.Album, error) { func (r *photoResolver) Album(ctx context.Context, obj *models.Photo) (*models.Album, error) {
panic("not implemented") row := r.Database.QueryRow("SELECT album.* from photo JOIN album ON photo.album_id = album.album_id WHERE photo_id = ?", obj.PhotoID)
return models.NewAlbumFromRow(row)
} }
func (r *photoResolver) Exif(ctx context.Context, obj *models.Photo) (*models.PhotoEXIF, error) { func (r *photoResolver) Exif(ctx context.Context, obj *models.Photo) (*models.PhotoEXIF, error) {

View File

@ -0,0 +1,75 @@
package resolvers
import (
"context"
"log"
"github.com/viktorstrate/photoview/api/graphql/auth"
"github.com/viktorstrate/photoview/api/graphql/models"
)
func (r *Resolver) Search(ctx context.Context, query string, _limitPhotos *int, _limitAlbums *int) (*models.SearchResult, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
limitPhotos := 10
limitAlbums := 10
if _limitPhotos != nil {
limitPhotos = *_limitPhotos
}
if _limitAlbums != nil {
limitAlbums = *_limitAlbums
}
wildQuery := "%" + query + "%"
photoRows, err := r.Database.Query(`
SELECT photo.* FROM photo JOIN album ON photo.album_id = album.album_id
WHERE album.owner_id = ? AND photo.title LIKE ? OR photo.path LIKE ?
ORDER BY (
case when photo.title LIKE ? then 2
when photo.path LIKE ? then 1
end ) DESC
LIMIT ?
`, user.UserID, wildQuery, wildQuery, wildQuery, wildQuery, limitPhotos)
if err != nil {
log.Printf("ERROR: searching photos %s", err)
return nil, err
}
photos, err := models.NewPhotosFromRows(photoRows)
if err != nil {
return nil, err
}
albumRows, err := r.Database.Query(`
SELECT * FROM album
WHERE owner_id = ? AND title LIKE ? OR path LIKE ?
ORDER BY (
case when title LIKE ? then 2
when path LIKE ? then 1
end ) DESC
LIMIT ?
`, user.UserID, wildQuery, wildQuery, wildQuery, wildQuery, limitAlbums)
if err != nil {
log.Printf("ERROR: searching albums %s", err)
return nil, err
}
albums, err := models.NewAlbumsFromRows(albumRows)
if err != nil {
return nil, err
}
result := models.SearchResult{
Query: query,
Photos: photos,
Albums: albums,
}
return &result, nil
}

View File

@ -39,6 +39,8 @@ type Query {
photo(id: Int!): Photo! photo(id: Int!): Photo!
shareToken(token: String!, password: String): ShareToken! shareToken(token: String!, password: String): ShareToken!
search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
} }
type Mutation { type Mutation {
@ -228,3 +230,9 @@ type PhotoEXIF {
"An index describing the mode for adjusting the exposure of the image" "An index describing the mode for adjusting the exposure of the image"
exposureProgram: Int exposureProgram: Int
} }
type SearchResult {
query: String!
albums: [Album!]!
photos: [Photo!]!
}

View File

@ -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",

View File

@ -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>
) )

View File

@ -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>
{localStorage.getItem('token') ? <SearchBar /> : null}
</Container>
)
export default Header

View File

@ -0,0 +1,235 @@
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) {
query
albums {
id
title
thumbnail {
thumbnail {
url
}
}
}
photos {
id
title
thumbnail {
url
}
album {
id
}
}
}
}
`
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 query = data && data.search.query
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} query={query} album={album} />
))
const photoElements = photos.map(photo => (
<PhotoRow key={photo.id} query={query} photo={photo} />
))
return (
<Results
onMouseDown={e => {
// Prevent input blur event
e.preventDefault()
}}
show={data}
>
{message}
{albumElements.length > 0 && <ResultTitle>Albums</ResultTitle>}
{albumElements}
{photoElements.length > 0 && <ResultTitle>Photos</ResultTitle>}
{photoElements}
</Results>
)
}
SearchResults.propTypes = {
result: PropTypes.object,
}
const RowLink = styled(NavLink)`
display: flex;
align-items: center;
color: black;
`
const PhotoSearchThumbnail = styled(ProtectedImage)`
width: 50px;
height: 50px;
margin: 2px 0;
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 = ({ query, photo }) => (
<RowLink to={`/album/${photo.album.id}`}>
<PhotoSearchThumbnail src={photo.thumbnail.url} />
<RowTitle>{searchHighlighted(query, photo.title)}</RowTitle>
</RowLink>
)
PhotoRow.propTypes = {
query: PropTypes.string.isRequired,
photo: PropTypes.object.isRequired,
}
const AlbumRow = ({ query, album }) => (
<RowLink to={`/album/${album.id}`}>
<AlbumSearchThumbnail src={album.thumbnail.thumbnail.url} />
<RowTitle>{searchHighlighted(query, album.title)}</RowTitle>
</RowLink>
)
AlbumRow.propTypes = {
query: PropTypes.string.isRequired,
album: PropTypes.object.isRequired,
}
const searchHighlighted = (query, text) => {
const i = text.toLowerCase().indexOf(query.toLowerCase())
if (i == -1) {
return text
}
const start = text.substring(0, i)
const middle = text.substring(i, i + query.length)
const end = text.substring(i + query.length)
return (
<>
{start}
<b>{middle}</b>
{end}
</>
)
}
export default SearchBar