commit
225f22bcfe
|
@ -140,6 +140,7 @@ type ComplexityRoot struct {
|
|||
MyPhotos func(childComplexity int, filter *models.Filter) int
|
||||
MyUser func(childComplexity 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
|
||||
SiteInfo func(childComplexity int) int
|
||||
User func(childComplexity int, filter *models.Filter) int
|
||||
|
@ -152,6 +153,12 @@ type ComplexityRoot struct {
|
|||
Success func(childComplexity int) int
|
||||
}
|
||||
|
||||
SearchResult struct {
|
||||
Albums func(childComplexity int) int
|
||||
Photos func(childComplexity int) int
|
||||
Query func(childComplexity int) int
|
||||
}
|
||||
|
||||
ShareToken struct {
|
||||
Album 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)
|
||||
Photo(ctx context.Context, id int) (*models.Photo, 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 {
|
||||
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
|
||||
|
||||
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":
|
||||
if e.complexity.Query.ShareToken == nil {
|
||||
break
|
||||
|
@ -829,6 +849,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||
|
||||
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":
|
||||
if e.complexity.ShareToken.Album == nil {
|
||||
break
|
||||
|
@ -1045,6 +1086,8 @@ type Query {
|
|||
photo(id: Int!): Photo!
|
||||
|
||||
shareToken(token: String!, password: String): ShareToken!
|
||||
|
||||
search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
@ -1234,6 +1277,12 @@ type PhotoEXIF {
|
|||
"An index describing the mode for adjusting the exposure of the image"
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
var err error
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
ctx = ec.Tracer.StartFieldExecution(ctx, field)
|
||||
defer func() {
|
||||
|
@ -4542,6 +4665,117 @@ func (ec *executionContext) _ScannerResult_message(ctx context.Context, field gr
|
|||
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) {
|
||||
ctx = ec.Tracer.StartFieldExecution(ctx, field)
|
||||
defer func() {
|
||||
|
@ -6836,6 +7070,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
|||
}
|
||||
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":
|
||||
out.Values[i] = ec._Query___type(ctx, field)
|
||||
case "__schema":
|
||||
|
@ -6887,6 +7135,43 @@ func (ec *executionContext) _ScannerResult(ctx context.Context, sel ast.Selectio
|
|||
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"}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return ec._ShareToken(ctx, sel, &v)
|
||||
}
|
||||
|
|
|
@ -47,6 +47,12 @@ type ScannerResult struct {
|
|||
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
|
||||
type SiteInfo struct {
|
||||
InitialSetup bool `json:"initialSetup"`
|
||||
|
|
|
@ -152,30 +152,9 @@ func (r *photoResolver) Thumbnail(ctx context.Context, obj *models.Photo) (*mode
|
|||
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) {
|
||||
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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -39,6 +39,8 @@ type Query {
|
|||
photo(id: Int!): Photo!
|
||||
|
||||
shareToken(token: String!, password: String): ShareToken!
|
||||
|
||||
search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
@ -228,3 +230,9 @@ type PhotoEXIF {
|
|||
"An index describing the mode for adjusting the exposure of the image"
|
||||
exposureProgram: Int
|
||||
}
|
||||
|
||||
type SearchResult {
|
||||
query: String!
|
||||
albums: [Album!]!
|
||||
photos: [Photo!]!
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"downloadjs": "^1.4.7",
|
||||
"graphql": "^14.6.0",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"lodash": "^4.17.15",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^1.19.1",
|
||||
"prop-types": "^15.7.2",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
|
@ -8,6 +8,7 @@ import { Query } from 'react-apollo'
|
|||
import gql from 'graphql-tag'
|
||||
import { Authorized } from './AuthorizedRoute'
|
||||
import Helmet from 'react-helmet'
|
||||
import Header from './components/header/Header'
|
||||
|
||||
const adminQuery = gql`
|
||||
query adminQuery {
|
||||
|
@ -76,21 +77,6 @@ const SideButtonLabel = styled.div`
|
|||
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 }) => (
|
||||
<Container>
|
||||
<Helmet>
|
||||
|
@ -132,9 +118,7 @@ const Layout = ({ children, title }) => (
|
|||
<div style={{ height: 24 }}></div>
|
||||
</Content>
|
||||
</Sidebar>
|
||||
<Header>
|
||||
<Title>Photoview</Title>
|
||||
</Header>
|
||||
<Header />
|
||||
</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>
|
||||
{localStorage.getItem('token') ? <SearchBar /> : null}
|
||||
</Container>
|
||||
)
|
||||
|
||||
export default Header
|
|
@ -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
|
Loading…
Reference in New Issue