Merge pull request #90 from viktorstrate/geographic-map-page
Geographic map page
This commit is contained in:
commit
54bfc3d480
|
@ -14,6 +14,10 @@ SERVE_UI=0
|
||||||
# When SERVE_UI is 1, PUBLIC_ENDPOINT is used instead of API_ENDPOINT and UI_ENDPOINT
|
# When SERVE_UI is 1, PUBLIC_ENDPOINT is used instead of API_ENDPOINT and UI_ENDPOINT
|
||||||
#PUBLIC_ENDPOINT=http://localhost:4001/
|
#PUBLIC_ENDPOINT=http://localhost:4001/
|
||||||
|
|
||||||
|
# Enter a valid mapbox token, to enable maps feature
|
||||||
|
# A token can be created for free at https://mapbox.com
|
||||||
|
#MAPBOX_TOKEN=<insert mapbox token here>
|
||||||
|
|
||||||
# Set to 1 to set server in development mode, this enables graphql playground
|
# Set to 1 to set server in development mode, this enables graphql playground
|
||||||
# Remove this if running in production
|
# Remove this if running in production
|
||||||
DEVELOPMENT=1
|
DEVELOPMENT=1
|
||||||
|
|
|
@ -144,9 +144,12 @@ type ComplexityRoot struct {
|
||||||
|
|
||||||
Query struct {
|
Query struct {
|
||||||
Album func(childComplexity int, id int) int
|
Album func(childComplexity int, id int) int
|
||||||
|
MapboxToken func(childComplexity int) int
|
||||||
Media func(childComplexity int, id int) int
|
Media func(childComplexity int, id int) int
|
||||||
|
MediaList func(childComplexity int, ids []int) int
|
||||||
MyAlbums func(childComplexity int, filter *models.Filter, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) int
|
MyAlbums func(childComplexity int, filter *models.Filter, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) int
|
||||||
MyMedia func(childComplexity int, filter *models.Filter) int
|
MyMedia func(childComplexity int, filter *models.Filter) int
|
||||||
|
MyMediaGeoJSON func(childComplexity int) int
|
||||||
MyUser func(childComplexity int) int
|
MyUser func(childComplexity int) int
|
||||||
Search func(childComplexity int, query string, limitMedia *int, limitAlbums *int) int
|
Search func(childComplexity int, query string, limitMedia *int, limitAlbums *int) int
|
||||||
ShareToken func(childComplexity int, token string, password *string) int
|
ShareToken func(childComplexity int, token string, password *string) int
|
||||||
|
@ -255,6 +258,9 @@ type QueryResolver interface {
|
||||||
Album(ctx context.Context, id int) (*models.Album, error)
|
Album(ctx context.Context, id int) (*models.Album, error)
|
||||||
MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error)
|
MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error)
|
||||||
Media(ctx context.Context, id int) (*models.Media, error)
|
Media(ctx context.Context, id int) (*models.Media, error)
|
||||||
|
MediaList(ctx context.Context, ids []int) ([]*models.Media, error)
|
||||||
|
MyMediaGeoJSON(ctx context.Context) (interface{}, error)
|
||||||
|
MapboxToken(ctx context.Context) (*string, error)
|
||||||
ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error)
|
ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error)
|
||||||
ShareTokenValidatePassword(ctx context.Context, token string, password *string) (bool, error)
|
ShareTokenValidatePassword(ctx context.Context, token string, password *string) (bool, error)
|
||||||
Search(ctx context.Context, query string, limitMedia *int, limitAlbums *int) (*models.SearchResult, error)
|
Search(ctx context.Context, query string, limitMedia *int, limitAlbums *int) (*models.SearchResult, error)
|
||||||
|
@ -846,6 +852,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||||
|
|
||||||
return e.complexity.Query.Album(childComplexity, args["id"].(int)), true
|
return e.complexity.Query.Album(childComplexity, args["id"].(int)), true
|
||||||
|
|
||||||
|
case "Query.mapboxToken":
|
||||||
|
if e.complexity.Query.MapboxToken == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Query.MapboxToken(childComplexity), true
|
||||||
|
|
||||||
case "Query.media":
|
case "Query.media":
|
||||||
if e.complexity.Query.Media == nil {
|
if e.complexity.Query.Media == nil {
|
||||||
break
|
break
|
||||||
|
@ -858,6 +871,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||||
|
|
||||||
return e.complexity.Query.Media(childComplexity, args["id"].(int)), true
|
return e.complexity.Query.Media(childComplexity, args["id"].(int)), true
|
||||||
|
|
||||||
|
case "Query.mediaList":
|
||||||
|
if e.complexity.Query.MediaList == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := ec.field_Query_mediaList_args(context.TODO(), rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Query.MediaList(childComplexity, args["ids"].([]int)), true
|
||||||
|
|
||||||
case "Query.myAlbums":
|
case "Query.myAlbums":
|
||||||
if e.complexity.Query.MyAlbums == nil {
|
if e.complexity.Query.MyAlbums == nil {
|
||||||
break
|
break
|
||||||
|
@ -882,6 +907,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||||
|
|
||||||
return e.complexity.Query.MyMedia(childComplexity, args["filter"].(*models.Filter)), true
|
return e.complexity.Query.MyMedia(childComplexity, args["filter"].(*models.Filter)), true
|
||||||
|
|
||||||
|
case "Query.myMediaGeoJson":
|
||||||
|
if e.complexity.Query.MyMediaGeoJSON == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Query.MyMediaGeoJSON(childComplexity), true
|
||||||
|
|
||||||
case "Query.myUser":
|
case "Query.myUser":
|
||||||
if e.complexity.Query.MyUser == nil {
|
if e.complexity.Query.MyUser == nil {
|
||||||
break
|
break
|
||||||
|
@ -1252,6 +1284,7 @@ var sources = []*ast.Source{
|
||||||
{Name: "graphql/schema.graphql", Input: `directive @isAdmin on FIELD_DEFINITION
|
{Name: "graphql/schema.graphql", Input: `directive @isAdmin on FIELD_DEFINITION
|
||||||
|
|
||||||
scalar Time
|
scalar Time
|
||||||
|
scalar Any
|
||||||
|
|
||||||
enum OrderDirection {
|
enum OrderDirection {
|
||||||
ASC
|
ASC
|
||||||
|
@ -1291,6 +1324,14 @@ type Query {
|
||||||
"Get media by id, user must own the media or be admin"
|
"Get media by id, user must own the media or be admin"
|
||||||
media(id: Int!): Media!
|
media(id: Int!): Media!
|
||||||
|
|
||||||
|
"Get a list of media by their ids, user must own the media or be admin"
|
||||||
|
mediaList(ids: [Int!]!): [Media!]!
|
||||||
|
|
||||||
|
"Get media owned by the logged in user, returned in GeoJson format"
|
||||||
|
myMediaGeoJson: Any!
|
||||||
|
"Get the mapbox api token, returns null if mapbox is not enabled"
|
||||||
|
mapboxToken: String
|
||||||
|
|
||||||
shareToken(token: String!, password: String): ShareToken!
|
shareToken(token: String!, password: String): ShareToken!
|
||||||
shareTokenValidatePassword(token: String!, password: String): Boolean!
|
shareTokenValidatePassword(token: String!, password: String): Boolean!
|
||||||
|
|
||||||
|
@ -1560,6 +1601,7 @@ func (ec *executionContext) field_Album_media_args(ctx context.Context, rawArgs
|
||||||
args["filter"] = arg0
|
args["filter"] = arg0
|
||||||
var arg1 *bool
|
var arg1 *bool
|
||||||
if tmp, ok := rawArgs["onlyFavorites"]; ok {
|
if tmp, ok := rawArgs["onlyFavorites"]; ok {
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("onlyFavorites"))
|
||||||
arg1, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
|
arg1, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1986,6 +2028,21 @@ func (ec *executionContext) field_Query_album_args(ctx context.Context, rawArgs
|
||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) field_Query_mediaList_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
var err error
|
||||||
|
args := map[string]interface{}{}
|
||||||
|
var arg0 []int
|
||||||
|
if tmp, ok := rawArgs["ids"]; ok {
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ids"))
|
||||||
|
arg0, err = ec.unmarshalNInt2ᚕintᚄ(ctx, tmp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args["ids"] = arg0
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) field_Query_media_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
func (ec *executionContext) field_Query_media_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{}{}
|
||||||
|
@ -2033,6 +2090,7 @@ func (ec *executionContext) field_Query_myAlbums_args(ctx context.Context, rawAr
|
||||||
args["showEmpty"] = arg2
|
args["showEmpty"] = arg2
|
||||||
var arg3 *bool
|
var arg3 *bool
|
||||||
if tmp, ok := rawArgs["onlyWithFavorites"]; ok {
|
if tmp, ok := rawArgs["onlyWithFavorites"]; ok {
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("onlyWithFavorites"))
|
||||||
arg3, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
|
arg3, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -4941,6 +4999,115 @@ func (ec *executionContext) _Query_media(ctx context.Context, field graphql.Coll
|
||||||
return ec.marshalNMedia2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res)
|
return ec.marshalNMedia2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Query_mediaList(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fc := &graphql.FieldContext{
|
||||||
|
Object: "Query",
|
||||||
|
Field: field,
|
||||||
|
Args: nil,
|
||||||
|
IsMethod: true,
|
||||||
|
IsResolver: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
rawArgs := field.ArgumentMap(ec.Variables)
|
||||||
|
args, err := ec.field_Query_mediaList_args(ctx, rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
fc.Args = args
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return ec.resolvers.Query().MediaList(rctx, args["ids"].([]int))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.([]*models.Media)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNMedia2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaᚄ(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Query_myMediaGeoJson(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fc := &graphql.FieldContext{
|
||||||
|
Object: "Query",
|
||||||
|
Field: field,
|
||||||
|
Args: nil,
|
||||||
|
IsMethod: true,
|
||||||
|
IsResolver: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return ec.resolvers.Query().MyMediaGeoJSON(rctx)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(interface{})
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNAny2interface(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Query_mapboxToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fc := &graphql.FieldContext{
|
||||||
|
Object: "Query",
|
||||||
|
Field: field,
|
||||||
|
Args: nil,
|
||||||
|
IsMethod: true,
|
||||||
|
IsResolver: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return ec.resolvers.Query().MapboxToken(rctx)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*string)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Query_shareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Query_shareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
@ -8113,6 +8280,45 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
|
case "mediaList":
|
||||||
|
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_mediaList(ctx, field)
|
||||||
|
if res == graphql.Null {
|
||||||
|
atomic.AddUint32(&invalids, 1)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
case "myMediaGeoJson":
|
||||||
|
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_myMediaGeoJson(ctx, field)
|
||||||
|
if res == graphql.Null {
|
||||||
|
atomic.AddUint32(&invalids, 1)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
case "mapboxToken":
|
||||||
|
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_mapboxToken(ctx, field)
|
||||||
|
return res
|
||||||
|
})
|
||||||
case "shareToken":
|
case "shareToken":
|
||||||
field := field
|
field := field
|
||||||
out.Concurrently(i, func() (res graphql.Marshaler) {
|
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||||
|
@ -8779,6 +8985,27 @@ func (ec *executionContext) marshalNAlbum2ᚖgithubᚗcomᚋviktorstrateᚋphoto
|
||||||
return ec._Album(ctx, sel, v)
|
return ec._Album(ctx, sel, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalNAny2interface(ctx context.Context, v interface{}) (interface{}, error) {
|
||||||
|
res, err := graphql.UnmarshalAny(v)
|
||||||
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNAny2interface(ctx context.Context, sel ast.SelectionSet, v interface{}) graphql.Marshaler {
|
||||||
|
if v == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := graphql.MarshalAny(v)
|
||||||
|
if res == graphql.Null {
|
||||||
|
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalNAuthorizeResult2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx context.Context, sel ast.SelectionSet, v models.AuthorizeResult) graphql.Marshaler {
|
func (ec *executionContext) marshalNAuthorizeResult2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAuthorizeResult(ctx context.Context, sel ast.SelectionSet, v models.AuthorizeResult) graphql.Marshaler {
|
||||||
return ec._AuthorizeResult(ctx, sel, &v)
|
return ec._AuthorizeResult(ctx, sel, &v)
|
||||||
}
|
}
|
||||||
|
@ -8838,6 +9065,36 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalNInt2ᚕintᚄ(ctx context.Context, v interface{}) ([]int, error) {
|
||||||
|
var vSlice []interface{}
|
||||||
|
if v != nil {
|
||||||
|
if tmp1, ok := v.([]interface{}); ok {
|
||||||
|
vSlice = tmp1
|
||||||
|
} else {
|
||||||
|
vSlice = []interface{}{v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
res := make([]int, len(vSlice))
|
||||||
|
for i := range vSlice {
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
|
||||||
|
res[i], err = ec.unmarshalNInt2int(ctx, vSlice[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNInt2ᚕintᚄ(ctx context.Context, sel ast.SelectionSet, v []int) graphql.Marshaler {
|
||||||
|
ret := make(graphql.Array, len(v))
|
||||||
|
for i := range v {
|
||||||
|
ret[i] = ec.marshalNInt2int(ctx, sel, v[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalNMedia2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx context.Context, sel ast.SelectionSet, v models.Media) graphql.Marshaler {
|
func (ec *executionContext) marshalNMedia2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx context.Context, sel ast.SelectionSet, v models.Media) graphql.Marshaler {
|
||||||
return ec._Media(ctx, sel, &v)
|
return ec._Media(ctx, sel, &v)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,44 @@ func (r *queryResolver) Media(ctx context.Context, id int) (*models.Media, error
|
||||||
return media, nil
|
return media, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) MediaList(ctx context.Context, ids []int) ([]*models.Media, error) {
|
||||||
|
user := auth.UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil, auth.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil, errors.New("no ids provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaIDQuestions := strings.Repeat("?,", len(ids))[:len(ids)*2-1]
|
||||||
|
|
||||||
|
queryArgs := make([]interface{}, 0)
|
||||||
|
for _, id := range ids {
|
||||||
|
queryArgs = append(queryArgs, id)
|
||||||
|
}
|
||||||
|
queryArgs = append(queryArgs, user.UserID)
|
||||||
|
|
||||||
|
rows, err := r.Database.Query(`
|
||||||
|
SELECT media.* FROM media
|
||||||
|
JOIN album ON media.album_id = album.album_id
|
||||||
|
WHERE media.media_id IN (`+mediaIDQuestions+`) AND album.owner_id = ?
|
||||||
|
AND media.media_id IN (
|
||||||
|
SELECT media_id FROM media_url WHERE media_url.media_id = media.media_id
|
||||||
|
)
|
||||||
|
`, queryArgs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not get media list by media_id and user_id from database")
|
||||||
|
}
|
||||||
|
|
||||||
|
media, err := models.NewMediaFromRows(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not convert database rows to media")
|
||||||
|
}
|
||||||
|
|
||||||
|
return media, nil
|
||||||
|
}
|
||||||
|
|
||||||
type mediaResolver struct {
|
type mediaResolver struct {
|
||||||
*Resolver
|
*Resolver
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/viktorstrate/photoview/api/graphql/auth"
|
||||||
|
"github.com/viktorstrate/photoview/api/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type geoJSONFeatureCollection struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Features []geoJSONFeature `json:"features"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type geoJSONFeature struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Properties interface{} `json:"properties"`
|
||||||
|
Geometry geoJSONFeatureGeometry `json:"geometry"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type geoJSONMediaProperties struct {
|
||||||
|
MediaID int `json:"media_id"`
|
||||||
|
MediaTitle string `json:"media_title"`
|
||||||
|
Thumbnail struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
} `json:"thumbnail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type geoJSONFeatureGeometry struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Coordinates [2]float64 `json:"coordinates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGeoJSONFeatureCollection(features []geoJSONFeature) geoJSONFeatureCollection {
|
||||||
|
return geoJSONFeatureCollection{
|
||||||
|
Type: "FeatureCollection",
|
||||||
|
Features: features,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGeoJSONFeature(properties interface{}, geometry geoJSONFeatureGeometry) geoJSONFeature {
|
||||||
|
return geoJSONFeature{
|
||||||
|
Type: "Feature",
|
||||||
|
Properties: properties,
|
||||||
|
Geometry: geometry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGeoJSONFeatureGeometryPoint(lat float64, long float64) geoJSONFeatureGeometry {
|
||||||
|
coordinates := [2]float64{long, lat}
|
||||||
|
|
||||||
|
return geoJSONFeatureGeometry{
|
||||||
|
Type: "Point",
|
||||||
|
Coordinates: coordinates,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) MyMediaGeoJSON(ctx context.Context) (interface{}, error) {
|
||||||
|
|
||||||
|
user := auth.UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil, errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := r.Database.Query(`
|
||||||
|
SELECT media.media_id, media.title,
|
||||||
|
url.media_name AS thumbnail_name, url.width AS thumbnail_width, url.height AS thumbnail_height,
|
||||||
|
exif.gps_latitude, exif.gps_longitude FROM media_exif exif
|
||||||
|
INNER JOIN media ON exif.exif_id = media.exif_id
|
||||||
|
INNER JOIN media_url url ON media.media_id = url.media_id
|
||||||
|
INNER JOIN album ON media.album_id = album.album_id
|
||||||
|
WHERE exif.gps_latitude IS NOT NULL
|
||||||
|
AND exif.gps_longitude IS NOT NULL
|
||||||
|
AND url.purpose = 'thumbnail'
|
||||||
|
AND album.owner_id = ?;
|
||||||
|
`, user.UserID)
|
||||||
|
defer rows.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
features := make([]geoJSONFeature, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
|
||||||
|
var mediaID int
|
||||||
|
var mediaTitle string
|
||||||
|
var thumbnailName string
|
||||||
|
var thumbnailWidth int
|
||||||
|
var thumbnailHeight int
|
||||||
|
var latitude float64
|
||||||
|
var longitude float64
|
||||||
|
|
||||||
|
if err := rows.Scan(&mediaID, &mediaTitle, &thumbnailName, &thumbnailWidth, &thumbnailHeight, &latitude, &longitude); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
geoPoint := makeGeoJSONFeatureGeometryPoint(latitude, longitude)
|
||||||
|
|
||||||
|
thumbnailURL := utils.ApiEndpointUrl()
|
||||||
|
thumbnailURL.Path = path.Join(thumbnailURL.Path, "photo", thumbnailName)
|
||||||
|
|
||||||
|
properties := geoJSONMediaProperties{
|
||||||
|
MediaID: mediaID,
|
||||||
|
MediaTitle: mediaTitle,
|
||||||
|
Thumbnail: struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
}{
|
||||||
|
URL: thumbnailURL.String(),
|
||||||
|
Width: thumbnailWidth,
|
||||||
|
Height: thumbnailHeight,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
features = append(features, makeGeoJSONFeature(properties, geoPoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
featureCollection := makeGeoJSONFeatureCollection(features)
|
||||||
|
|
||||||
|
return featureCollection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) MapboxToken(ctx context.Context) (*string, error) {
|
||||||
|
mapboxTokenEnv := os.Getenv("MAPBOX_TOKEN")
|
||||||
|
if mapboxTokenEnv == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mapboxTokenEnv, nil
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
directive @isAdmin on FIELD_DEFINITION
|
directive @isAdmin on FIELD_DEFINITION
|
||||||
|
|
||||||
scalar Time
|
scalar Time
|
||||||
|
scalar Any
|
||||||
|
|
||||||
enum OrderDirection {
|
enum OrderDirection {
|
||||||
ASC
|
ASC
|
||||||
|
@ -40,6 +41,14 @@ type Query {
|
||||||
"Get media by id, user must own the media or be admin"
|
"Get media by id, user must own the media or be admin"
|
||||||
media(id: Int!): Media!
|
media(id: Int!): Media!
|
||||||
|
|
||||||
|
"Get a list of media by their ids, user must own the media or be admin"
|
||||||
|
mediaList(ids: [Int!]!): [Media!]!
|
||||||
|
|
||||||
|
"Get media owned by the logged in user, returned in GeoJson format"
|
||||||
|
myMediaGeoJson: Any!
|
||||||
|
"Get the mapbox api token, returns null if mapbox is not enabled"
|
||||||
|
mapboxToken: String
|
||||||
|
|
||||||
shareToken(token: String!, password: String): ShareToken!
|
shareToken(token: String!, password: String): ShareToken!
|
||||||
shareTokenValidatePassword(token: String!, password: String): Boolean!
|
shareTokenValidatePassword(token: String!, password: String): Boolean!
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,11 @@ services:
|
||||||
# change this value to http://example.com/
|
# change this value to http://example.com/
|
||||||
- PUBLIC_ENDPOINT=http://localhost:8000/
|
- PUBLIC_ENDPOINT=http://localhost:8000/
|
||||||
|
|
||||||
|
# Optional: To enable map related features, you need to create a mapbox token.
|
||||||
|
# A token can be generated for free here https://account.mapbox.com/access-tokens/
|
||||||
|
# It's a good idea to limit the scope of the token to your own domain, to prevent others from using it.
|
||||||
|
# - MAPBOX_TOKEN=<YOUR TOKEN HERE>
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- api_cache:/app/cache
|
- api_cache:/app/cache
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"eslint-plugin-react-hooks": "^4.1.2",
|
"eslint-plugin-react-hooks": "^4.1.2",
|
||||||
"husky": "^4.3.0",
|
"husky": "^4.3.0",
|
||||||
"lint-staged": "^10.4.0",
|
"lint-staged": "^10.4.0",
|
||||||
|
"mapbox-gl": "^1.12.0",
|
||||||
"parcel-plugin-sw-cache": "^0.3.1",
|
"parcel-plugin-sw-cache": "^0.3.1",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.1.2",
|
||||||
"react-router-prop-types": "^1.0.5"
|
"react-router-prop-types": "^1.0.5"
|
||||||
|
|
|
@ -4,13 +4,13 @@ import styled from 'styled-components'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
import { Icon } from 'semantic-ui-react'
|
import { Icon } from 'semantic-ui-react'
|
||||||
import Sidebar from './components/sidebar/Sidebar'
|
import Sidebar from './components/sidebar/Sidebar'
|
||||||
import { Query } from 'react-apollo'
|
import { useQuery } 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'
|
import Header from './components/header/Header'
|
||||||
|
|
||||||
const adminQuery = gql`
|
const ADMIN_QUERY = gql`
|
||||||
query adminQuery {
|
query adminQuery {
|
||||||
myUser {
|
myUser {
|
||||||
admin
|
admin
|
||||||
|
@ -18,6 +18,12 @@ const adminQuery = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const MAPBOX_QUERY = gql`
|
||||||
|
query mapboxEnabledQuery {
|
||||||
|
mapboxToken
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -88,7 +94,16 @@ const SideButtonLabel = styled.div`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Layout = ({ children, title }) => (
|
const Layout = ({ children, title }) => {
|
||||||
|
const adminQuery = useQuery(ADMIN_QUERY)
|
||||||
|
const mapboxQuery = useQuery(MAPBOX_QUERY)
|
||||||
|
|
||||||
|
const isAdmin =
|
||||||
|
adminQuery.data && adminQuery.data.myUser && adminQuery.data.myUser.admin
|
||||||
|
|
||||||
|
const mapboxEnabled = mapboxQuery.data && mapboxQuery.data.mapboxToken != null
|
||||||
|
|
||||||
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{title ? `${title} - Photoview` : `Photoview`}</title>
|
<title>{title ? `${title} - Photoview` : `Photoview`}</title>
|
||||||
|
@ -103,20 +118,18 @@ const Layout = ({ children, title }) => (
|
||||||
<Icon name="images outline" />
|
<Icon name="images outline" />
|
||||||
<SideButtonLabel>Albums</SideButtonLabel>
|
<SideButtonLabel>Albums</SideButtonLabel>
|
||||||
</SideButton>
|
</SideButton>
|
||||||
<Query query={adminQuery}>
|
{mapboxEnabled ? (
|
||||||
{({ loading, error, data }) => {
|
<SideButton to="/places" exact>
|
||||||
if (data && data.myUser && data.myUser.admin) {
|
<Icon name="map outline" />
|
||||||
return (
|
<SideButtonLabel>Places</SideButtonLabel>
|
||||||
|
</SideButton>
|
||||||
|
) : null}
|
||||||
|
{isAdmin ? (
|
||||||
<SideButton to="/settings" exact>
|
<SideButton to="/settings" exact>
|
||||||
<Icon name="settings" />
|
<Icon name="settings" />
|
||||||
<SideButtonLabel>Settings</SideButtonLabel>
|
<SideButtonLabel>Settings</SideButtonLabel>
|
||||||
</SideButton>
|
</SideButton>
|
||||||
)
|
) : null}
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}}
|
|
||||||
</Query>
|
|
||||||
<SideButton to="/logout">
|
<SideButton to="/logout">
|
||||||
<Icon name="lock" />
|
<Icon name="lock" />
|
||||||
<SideButtonLabel>Log out</SideButtonLabel>
|
<SideButtonLabel>Log out</SideButtonLabel>
|
||||||
|
@ -131,7 +144,8 @@ const Layout = ({ children, title }) => (
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<Header />
|
<Header />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Layout.propTypes = {
|
Layout.propTypes = {
|
||||||
children: PropTypes.any.isRequired,
|
children: PropTypes.any.isRequired,
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import imagePopupSrc from './image-popup.svg'
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
width: 56px;
|
||||||
|
height: 68px;
|
||||||
|
position: relative;
|
||||||
|
margin-top: -54px;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ThumbnailImage = styled.img`
|
||||||
|
position: absolute;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
object-fit: cover;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PopupImage = styled.img`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PointCountCircle = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: -10px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #00b3dc;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 2px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MapClusterMarker = ({
|
||||||
|
thumbnail: thumbJson,
|
||||||
|
point_count_abbreviated,
|
||||||
|
cluster,
|
||||||
|
cluster_id,
|
||||||
|
media_id,
|
||||||
|
setPresentMarker,
|
||||||
|
}) => {
|
||||||
|
const thumbnail = JSON.parse(thumbJson)
|
||||||
|
|
||||||
|
const presentMedia = () => {
|
||||||
|
setPresentMarker({
|
||||||
|
cluster: !!cluster,
|
||||||
|
id: cluster ? cluster_id : media_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper onClick={presentMedia}>
|
||||||
|
<PopupImage src={imagePopupSrc} />
|
||||||
|
<ThumbnailImage src={thumbnail.url} />
|
||||||
|
{cluster && (
|
||||||
|
<PointCountCircle>{point_count_abbreviated}</PointCountCircle>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MapClusterMarker.propTypes = {
|
||||||
|
thumbnail: PropTypes.string,
|
||||||
|
cluster: PropTypes.bool,
|
||||||
|
point_count_abbreviated: PropTypes.number,
|
||||||
|
cluster_id: PropTypes.number,
|
||||||
|
media_id: PropTypes.number,
|
||||||
|
setPresentMarker: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapClusterMarker
|
|
@ -0,0 +1,114 @@
|
||||||
|
import React, { useEffect, useState, useRef } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import { useLazyQuery } from 'react-apollo'
|
||||||
|
import PresentView from '../../components/photoGallery/presentView/PresentView'
|
||||||
|
|
||||||
|
const QUERY_MEDIA = gql`
|
||||||
|
query placePageQueryMedia($mediaIDs: [Int!]!) {
|
||||||
|
mediaList(ids: $mediaIDs) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
thumbnail {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
highRes {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
videoWeb {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const getMediaFromMarker = (map, presentMarker) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const { cluster, id } = presentMarker
|
||||||
|
|
||||||
|
if (cluster) {
|
||||||
|
map
|
||||||
|
.getSource('media')
|
||||||
|
.getClusterLeaves(id, 1000, 0, (error, features) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const media = features.map(feat => feat.properties)
|
||||||
|
resolve(media)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const features = map.querySourceFeatures('media')
|
||||||
|
const media = features.find(f => f.properties.media_id == id).properties
|
||||||
|
resolve([media])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => {
|
||||||
|
const [mediaMarkers, setMediaMarkers] = useState(null)
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0)
|
||||||
|
|
||||||
|
const [loadMedia, { data: loadedMedia }] = useLazyQuery(QUERY_MEDIA)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (presentMarker == null || map == null) {
|
||||||
|
setMediaMarkers(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getMediaFromMarker(map, presentMarker).then(setMediaMarkers)
|
||||||
|
}, [presentMarker])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mediaMarkers) return
|
||||||
|
|
||||||
|
setCurrentIndex(0)
|
||||||
|
loadMedia({
|
||||||
|
variables: {
|
||||||
|
mediaIDs: mediaMarkers.map(x => x.media_id),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [mediaMarkers])
|
||||||
|
|
||||||
|
if (presentMarker == null || map == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadedMedia == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PresentView
|
||||||
|
media={loadedMedia.mediaList[currentIndex]}
|
||||||
|
nextImage={() => {
|
||||||
|
setCurrentIndex(i => Math.min(mediaMarkers.length - 1, i + 1))
|
||||||
|
}}
|
||||||
|
previousImage={() => {
|
||||||
|
setCurrentIndex(i => Math.max(0, i - 1))
|
||||||
|
}}
|
||||||
|
setPresenting={presenting => {
|
||||||
|
if (!presenting) {
|
||||||
|
setCurrentIndex(0)
|
||||||
|
setPresentMarker(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MapPresentMarker.propTypes = {
|
||||||
|
map: PropTypes.object,
|
||||||
|
presentMarker: PropTypes.object,
|
||||||
|
setPresentMarker: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapPresentMarker
|
|
@ -0,0 +1,126 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useQuery } from 'react-apollo'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||||
|
|
||||||
|
import Layout from '../../Layout'
|
||||||
|
import { makeUpdateMarkers } from './mapboxHelperFunctions'
|
||||||
|
import MapPresentMarker from './MapPresentMarker'
|
||||||
|
|
||||||
|
const MapWrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 24px);
|
||||||
|
`
|
||||||
|
|
||||||
|
const MapContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MAPBOX_DATA_QUERY = gql`
|
||||||
|
query placePageMapboxToken {
|
||||||
|
mapboxToken
|
||||||
|
myMediaGeoJson
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MapPage = () => {
|
||||||
|
const [mapboxLibrary, setMapboxLibrary] = useState(null)
|
||||||
|
const [presentMarker, setPresentMarker] = useState(null)
|
||||||
|
const mapContainer = useRef()
|
||||||
|
const map = useRef()
|
||||||
|
|
||||||
|
const { data: mapboxData } = useQuery(MAPBOX_DATA_QUERY)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadMapboxLibrary() {
|
||||||
|
const mapbox = await import('mapbox-gl')
|
||||||
|
setMapboxLibrary(mapbox)
|
||||||
|
}
|
||||||
|
loadMapboxLibrary()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
mapboxLibrary == null ||
|
||||||
|
mapContainer.current == null ||
|
||||||
|
mapboxData == null ||
|
||||||
|
map.current != null
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mapboxLibrary.accessToken = mapboxData.mapboxToken
|
||||||
|
|
||||||
|
map.current = new mapboxLibrary.Map({
|
||||||
|
container: mapContainer.current,
|
||||||
|
style: 'mapbox://styles/mapbox/streets-v11',
|
||||||
|
zoom: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
map.current.on('load', () => {
|
||||||
|
map.current.addSource('media', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: mapboxData.myMediaGeoJson,
|
||||||
|
cluster: true,
|
||||||
|
clusterRadius: 50,
|
||||||
|
clusterProperties: {
|
||||||
|
thumbnail: ['coalesce', ['get', 'thumbnail'], false],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add dummy layer for features to be queryable
|
||||||
|
map.current.addLayer({
|
||||||
|
id: 'media-points',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'media',
|
||||||
|
filter: ['!', true],
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateMarkers = makeUpdateMarkers({
|
||||||
|
map: map.current,
|
||||||
|
mapboxLibrary,
|
||||||
|
setPresentMarker,
|
||||||
|
})
|
||||||
|
|
||||||
|
map.current.on('move', updateMarkers)
|
||||||
|
map.current.on('moveend', updateMarkers)
|
||||||
|
map.current.on('sourcedata', updateMarkers)
|
||||||
|
updateMarkers()
|
||||||
|
})
|
||||||
|
}, [mapContainer, mapboxLibrary, mapboxData])
|
||||||
|
|
||||||
|
if (mapboxData && mapboxData.mapboxToken == null) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<h1>Mapbox token is not set</h1>
|
||||||
|
<p>
|
||||||
|
To use map related features a mapbox token is needed.
|
||||||
|
<br /> A mapbox token can be created for free at{' '}
|
||||||
|
<a href="https://account.mapbox.com/access-tokens/">mapbox.com</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Make sure the access token is added as the MAPBOX_TOKEN environment
|
||||||
|
variable.
|
||||||
|
</p>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<MapWrapper>
|
||||||
|
<MapContainer ref={mapContainer}></MapContainer>
|
||||||
|
</MapWrapper>
|
||||||
|
<MapPresentMarker
|
||||||
|
map={map.current}
|
||||||
|
presentMarker={presentMarker}
|
||||||
|
setPresentMarker={setPresentMarker}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapPage
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 56 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.875,0,0,1.0625,0,0)">
|
||||||
|
<g transform="matrix(1,0,0,0.970588,0,0)">
|
||||||
|
<g transform="matrix(0.857143,0,0,1.55496,8,-35.483)">
|
||||||
|
<g style="filter:url(#_Effect1);">
|
||||||
|
<ellipse cx="28" cy="61.172" rx="8" ry="0.828" style="fill-opacity:0.5;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.14286,0,0,0.969697,0,0)">
|
||||||
|
<path d="M4,54C3.47,54 2.961,53.789 2.586,53.414C2.211,53.039 2,52.53 2,52C2,43.732 2,12.268 2,4C2,3.47 2.211,2.961 2.586,2.586C2.961,2.211 3.47,2 4,2L52,2C52.53,2 53.039,2.211 53.414,2.586C53.789,2.961 54,3.47 54,4L54,52C54,52.53 53.789,53.039 53.414,53.414C53.039,53.789 52.53,54 52,54C48.077,54 39.521,54 36.828,54C36.298,54 35.789,54.211 35.414,54.586C34.161,55.839 31.192,58.808 29.414,60.586C29.039,60.961 28.53,61.172 28,61.172C27.47,61.172 26.961,60.961 26.586,60.586C24.808,58.808 21.839,55.839 20.586,54.586C20.211,54.211 19.702,54 19.172,54C16.479,54 7.923,54 4,54Z" style="fill:white;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="_Effect1" filterUnits="userSpaceOnUse" x="8" y="54.7306" width="40" height="12.8819">
|
||||||
|
<feGaussianBlur in="SourceGraphic" stdDeviation="1.1983"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import MapClusterMarker from './MapClusterMarker'
|
||||||
|
|
||||||
|
let markers = {}
|
||||||
|
let markersOnScreen = {}
|
||||||
|
|
||||||
|
export const makeUpdateMarkers = ({
|
||||||
|
map,
|
||||||
|
mapboxLibrary,
|
||||||
|
setPresentMarker,
|
||||||
|
}) => () => {
|
||||||
|
let newMarkers = {}
|
||||||
|
const features = map.querySourceFeatures('media')
|
||||||
|
|
||||||
|
// for every media on the screen, create an HTML marker for it (if we didn't yet),
|
||||||
|
// and add it to the map if it's not there already
|
||||||
|
for (let i = 0; i < features.length; i++) {
|
||||||
|
const coords = features[i].geometry.coordinates
|
||||||
|
const props = features[i].properties
|
||||||
|
const id = props.cluster
|
||||||
|
? `cluster_${props.cluster_id}`
|
||||||
|
: `media_${props.media_id}`
|
||||||
|
|
||||||
|
let marker = markers[id]
|
||||||
|
if (!marker) {
|
||||||
|
let el = createClusterPopupElement(props, setPresentMarker)
|
||||||
|
marker = markers[id] = new mapboxLibrary.Marker({
|
||||||
|
element: el,
|
||||||
|
}).setLngLat(coords)
|
||||||
|
}
|
||||||
|
newMarkers[id] = marker
|
||||||
|
|
||||||
|
if (!markersOnScreen[id]) marker.addTo(map)
|
||||||
|
}
|
||||||
|
// for every marker we've added previously, remove those that are no longer visible
|
||||||
|
for (const id in markersOnScreen) {
|
||||||
|
if (!newMarkers[id]) markersOnScreen[id].remove()
|
||||||
|
}
|
||||||
|
markersOnScreen = newMarkers
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClusterPopupElement(geojsonProps, setPresentMarker) {
|
||||||
|
const el = document.createElement('div')
|
||||||
|
ReactDOM.render(
|
||||||
|
<MapClusterMarker {...geojsonProps} setPresentMarker={setPresentMarker} />,
|
||||||
|
el
|
||||||
|
)
|
||||||
|
return el
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ const AlbumsPage = React.lazy(() => import('./Pages/AllAlbumsPage/AlbumsPage'))
|
||||||
const AlbumPage = React.lazy(() => import('./Pages/AlbumPage/AlbumPage'))
|
const AlbumPage = React.lazy(() => import('./Pages/AlbumPage/AlbumPage'))
|
||||||
const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute'))
|
const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute'))
|
||||||
const PhotosPage = React.lazy(() => import('./Pages/PhotosPage/PhotosPage'))
|
const PhotosPage = React.lazy(() => import('./Pages/PhotosPage/PhotosPage'))
|
||||||
|
const PlacesPage = React.lazy(() => import('./Pages/PlacesPage/PlacesPage'))
|
||||||
const SharePage = React.lazy(() => import('./Pages/SharePage/SharePage'))
|
const SharePage = React.lazy(() => import('./Pages/SharePage/SharePage'))
|
||||||
|
|
||||||
const LoginPage = React.lazy(() => import('./Pages/LoginPage/LoginPage'))
|
const LoginPage = React.lazy(() => import('./Pages/LoginPage/LoginPage'))
|
||||||
|
@ -43,6 +44,7 @@ class Routes extends React.Component {
|
||||||
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
|
<AuthorizedRoute exact path="/albums" component={AlbumsPage} />
|
||||||
<AuthorizedRoute path="/album/:id/:subPage?" component={AlbumPage} />
|
<AuthorizedRoute path="/album/:id/:subPage?" component={AlbumPage} />
|
||||||
<AuthorizedRoute path="/photos/:subPage?" component={PhotosPage} />
|
<AuthorizedRoute path="/photos/:subPage?" component={PhotosPage} />
|
||||||
|
<AuthorizedRoute path="/places" component={PlacesPage} />
|
||||||
<AuthorizedRoute admin path="/settings" component={SettingsPage} />
|
<AuthorizedRoute admin path="/settings" component={SettingsPage} />
|
||||||
<Route path="/" exact render={() => <Redirect to="/photos" />} />
|
<Route path="/" exact render={() => <Redirect to="/photos" />} />
|
||||||
<Route render={() => <div>Page not found</div>} />
|
<Route render={() => <div>Page not found</div>} />
|
||||||
|
|
|
@ -44,32 +44,6 @@ const PhotoGallery = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { updateSidebar } = useContext(SidebarContext)
|
const { updateSidebar } = useContext(SidebarContext)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const keyDownEvent = e => {
|
|
||||||
if (!onSelectImage || activeIndex == -1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key == 'ArrowRight') {
|
|
||||||
nextImage && nextImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key == 'ArrowLeft') {
|
|
||||||
nextImage && previousImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key == 'Escape' && presenting) {
|
|
||||||
setPresenting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', keyDownEvent)
|
|
||||||
|
|
||||||
return function cleanup() {
|
|
||||||
document.removeEventListener('keydown', keyDownEvent)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const activeImage = media && activeIndex != -1 && media[activeIndex]
|
const activeImage = media && activeIndex != -1 && media[activeIndex]
|
||||||
|
|
||||||
const getPhotoElements = updateSidebar => {
|
const getPhotoElements = updateSidebar => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React from 'react'
|
|
||||||
import styled, { createGlobalStyle } from 'styled-components'
|
import styled, { createGlobalStyle } from 'styled-components'
|
||||||
import PresentNavigationOverlay from './PresentNavigationOverlay'
|
import PresentNavigationOverlay from './PresentNavigationOverlay'
|
||||||
import PresentMedia from './PresentMedia'
|
import PresentMedia from './PresentMedia'
|
||||||
|
@ -28,14 +28,43 @@ const PresentView = ({
|
||||||
nextImage,
|
nextImage,
|
||||||
previousImage,
|
previousImage,
|
||||||
setPresenting,
|
setPresenting,
|
||||||
}) => (
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const keyDownEvent = e => {
|
||||||
|
if (e.key == 'ArrowRight') {
|
||||||
|
nextImage && nextImage()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key == 'ArrowLeft') {
|
||||||
|
nextImage && previousImage()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key == 'Escape') {
|
||||||
|
setPresenting(false)
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', keyDownEvent)
|
||||||
|
|
||||||
|
return function cleanup() {
|
||||||
|
document.removeEventListener('keydown', keyDownEvent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
<StyledContainer {...className}>
|
<StyledContainer {...className}>
|
||||||
<PreventScroll />
|
<PreventScroll />
|
||||||
<PresentNavigationOverlay {...{ nextImage, previousImage, setPresenting }}>
|
<PresentNavigationOverlay
|
||||||
|
{...{ nextImage, previousImage, setPresenting }}
|
||||||
|
>
|
||||||
<PresentMedia media={media} imageLoaded={imageLoaded} />
|
<PresentMedia media={media} imageLoaded={imageLoaded} />
|
||||||
</PresentNavigationOverlay>
|
</PresentNavigationOverlay>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
PresentView.propTypes = {
|
PresentView.propTypes = {
|
||||||
media: PropTypes.object.isRequired,
|
media: PropTypes.object.isRequired,
|
||||||
|
|
Loading…
Reference in New Issue