mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 21:09:05 +00:00
Add right sidebar
This commit is contained in:
@@ -39,6 +39,27 @@ const myAlbumQuery = function(args, ctx, info) {
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const myPhotoQuery = function(args, ctx, info) {
|
||||||
|
const query = cypherQuery(args, ctx, info)
|
||||||
|
|
||||||
|
const whereSplit = query[0].indexOf('RETURN')
|
||||||
|
|
||||||
|
query[0] = injectAt(
|
||||||
|
query[0],
|
||||||
|
whereSplit,
|
||||||
|
`MATCH (u:User { id: {userid} }) WHERE (u)-[:OWNS]->(:Album)-[:CONTAINS]->(photo) `
|
||||||
|
)
|
||||||
|
query[1].userid = ctx.user.id
|
||||||
|
|
||||||
|
query[0] = injectAt(
|
||||||
|
query[0],
|
||||||
|
query[0].indexOf('RETURN `photo` {') + 16,
|
||||||
|
query[0].indexOf('RETURN `photo` {}') == -1 ? ` .id, ` : ` .id `
|
||||||
|
)
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
const Query = {
|
const Query = {
|
||||||
async myAlbums(root, args, ctx, info) {
|
async myAlbums(root, args, ctx, info) {
|
||||||
let query = myAlbumQuery(args, ctx, info)
|
let query = myAlbumQuery(args, ctx, info)
|
||||||
@@ -73,6 +94,39 @@ const Query = {
|
|||||||
|
|
||||||
return result.records[0].get('album')
|
return result.records[0].get('album')
|
||||||
},
|
},
|
||||||
|
async myPhotos(root, args, ctx, info) {
|
||||||
|
let query = myPhotoQuery(args, ctx, info)
|
||||||
|
console.log(query)
|
||||||
|
|
||||||
|
const session = ctx.driver.session()
|
||||||
|
|
||||||
|
const result = await session.run(...query)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
return result.records.map(record => record.get('photo'))
|
||||||
|
},
|
||||||
|
async photo(root, args, ctx, info) {
|
||||||
|
const session = ctx.driver.session()
|
||||||
|
|
||||||
|
let query = myPhotoQuery(args, ctx, info)
|
||||||
|
|
||||||
|
const whereSplit = query[0].indexOf('RETURN')
|
||||||
|
|
||||||
|
query[0] = injectAt(query[0], whereSplit, ` AND photo.id = {id} `)
|
||||||
|
query[1].id = args.id
|
||||||
|
console.log(query)
|
||||||
|
|
||||||
|
const result = await session.run(...query)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
if (result.records.length == 0) {
|
||||||
|
throw new Error('Album was not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.records[0].get('photo')
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function photoResolver(image) {
|
function photoResolver(image) {
|
||||||
|
|||||||
@@ -75,4 +75,7 @@ type Query {
|
|||||||
|
|
||||||
myAlbums: [Album] @isAuthenticated
|
myAlbums: [Album] @isAuthenticated
|
||||||
album(id: ID): Album @isAuthenticated
|
album(id: ID): Album @isAuthenticated
|
||||||
|
|
||||||
|
myPhotos: [Photo] @isAuthenticated
|
||||||
|
photo(id: ID): Photo @isAuthenticated
|
||||||
}
|
}
|
||||||
|
|||||||
25
ui/src/AuthorizedRoute.js
Normal file
25
ui/src/AuthorizedRoute.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Route, Redirect } from 'react-router-dom'
|
||||||
|
|
||||||
|
const AuthorizedRoute = ({ component: Component, ...props }) => {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
let unauthorizedRedirect = null
|
||||||
|
if (!token) {
|
||||||
|
unauthorizedRedirect = <Redirect to="/login" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
{...props}
|
||||||
|
render={routeProps => (
|
||||||
|
<>
|
||||||
|
{unauthorizedRedirect}
|
||||||
|
<Component {...routeProps} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
></Route>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthorizedRoute
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Link } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
|
import { Icon } from 'semantic-ui-react'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin-right: 500px;
|
||||||
/* display: grid;
|
/* display: grid;
|
||||||
grid-template-columns: 80px 1fr 500px; */
|
grid-template-columns: 80px 1fr 500px; */
|
||||||
`
|
`
|
||||||
@@ -17,31 +19,42 @@ const LeftSidebar = styled.div`
|
|||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const RightSidebar = styled.div`
|
|
||||||
height: 100%;
|
|
||||||
width: 500px;
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: 60px;
|
|
||||||
background-color: white;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Content = styled.div`
|
const Content = styled.div`
|
||||||
margin-top: 60px;
|
margin-top: 60px;
|
||||||
margin-left: 80px;
|
margin-left: 80px;
|
||||||
margin-right: 500px;
|
|
||||||
padding: 12px 8px 0;
|
padding: 12px 8px 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SideButton = styled(Link)`
|
const SideButton = props => {
|
||||||
border: 1px solid #eee;
|
const StyledLink = styled(NavLink)`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 17px;
|
padding-top: 8px;
|
||||||
border-radius: 50%;
|
padding-left: 2px;
|
||||||
display: block;
|
display: block;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
|
||||||
|
font-size: 28px;
|
||||||
|
|
||||||
|
color: #888;
|
||||||
|
|
||||||
|
transition: transform 200ms, box-shadow 200ms;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledLink {...props} activeStyle={{ color: '#4183c4' }}>
|
||||||
|
{props.children}
|
||||||
|
</StyledLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SideButtonLabel = styled.div`
|
||||||
|
font-size: 16px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Header = styled.div`
|
const Header = styled.div`
|
||||||
@@ -64,10 +77,12 @@ class Layout extends Component {
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<LeftSidebar>
|
<LeftSidebar>
|
||||||
<SideButton to="/">Albums</SideButton>
|
<SideButton to="/" exact>
|
||||||
|
<Icon name="images outline" />
|
||||||
|
<SideButtonLabel>Albums</SideButtonLabel>
|
||||||
|
</SideButton>
|
||||||
</LeftSidebar>
|
</LeftSidebar>
|
||||||
<Content>{this.props.children}</Content>
|
<Content>{this.props.children}</Content>
|
||||||
<RightSidebar>Right</RightSidebar>
|
|
||||||
<Header>
|
<Header>
|
||||||
<Title>Photoview</Title>
|
<Title>Photoview</Title>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { Query } from 'react-apollo'
|
import { Query } from 'react-apollo'
|
||||||
import { Gallery, Photo, PhotoFiller } from './styledElements'
|
import {
|
||||||
|
Gallery,
|
||||||
|
Photo,
|
||||||
|
PhotoFiller,
|
||||||
|
PhotoContainer,
|
||||||
|
PhotoOverlay,
|
||||||
|
} from './styledElements'
|
||||||
import Layout from '../../Layout'
|
import Layout from '../../Layout'
|
||||||
|
import { Loader } from 'semantic-ui-react'
|
||||||
|
import AlbumSidebar from './AlbumSidebar'
|
||||||
|
|
||||||
const albumQuery = gql`
|
const albumQuery = gql`
|
||||||
query albumQuery($id: ID) {
|
query albumQuery($id: ID) {
|
||||||
@@ -33,7 +41,7 @@ class AlbumPage extends Component {
|
|||||||
this.photoAmount = 1
|
this.photoAmount = 1
|
||||||
this.previousActive = false
|
this.previousActive = false
|
||||||
|
|
||||||
this.keyUpEvent = e => {
|
this.keyDownEvent = e => {
|
||||||
const activeImage = this.state.activeImage
|
const activeImage = this.state.activeImage
|
||||||
if (activeImage != -1) {
|
if (activeImage != -1) {
|
||||||
if (e.key == 'ArrowRight') {
|
if (e.key == 'ArrowRight') {
|
||||||
@@ -52,11 +60,11 @@ class AlbumPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('keyup', this.keyUpEvent)
|
document.addEventListener('keydown', this.keyDownEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('keyup', this.keyUpEvent)
|
document.removeEventListener('keydown', this.keyDownEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveImage(index) {
|
setActiveImage(index) {
|
||||||
@@ -80,23 +88,45 @@ class AlbumPage extends Component {
|
|||||||
<Query query={albumQuery} variables={{ id: albumId }}>
|
<Query query={albumQuery} variables={{ id: albumId }}>
|
||||||
{({ loading, error, data }) => {
|
{({ loading, error, data }) => {
|
||||||
if (error) return <div>Error</div>
|
if (error) return <div>Error</div>
|
||||||
if (loading) return <div>Loading</div>
|
|
||||||
|
|
||||||
this.photoAmount = data.album.photos.length
|
let photos = null
|
||||||
|
if (data.album) {
|
||||||
|
this.photoAmount = data.album.photos.length
|
||||||
|
|
||||||
const { activeImage } = this.state
|
const { activeImage } = this.state
|
||||||
|
|
||||||
const photos = data.album.photos.map((photo, index) => {
|
photos = data.album.photos.map((photo, index) => {
|
||||||
return <Photo key={photo.id} src={photo.thumbnail.path}></Photo>
|
const active = activeImage == index
|
||||||
})
|
|
||||||
|
return (
|
||||||
|
<PhotoContainer
|
||||||
|
key={photo.id}
|
||||||
|
onClick={() => {
|
||||||
|
this.setActiveImage(index)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Photo src={photo.thumbnail.path} />
|
||||||
|
<PhotoOverlay active={active} />
|
||||||
|
</PhotoContainer>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>{data.album.title}</h1>
|
<h1>{data.album ? data.album.title : ''}</h1>
|
||||||
<Gallery>
|
<Gallery>
|
||||||
|
<Loader active={loading}>Loading images</Loader>
|
||||||
{photos}
|
{photos}
|
||||||
<PhotoFiller />
|
<PhotoFiller />
|
||||||
</Gallery>
|
</Gallery>
|
||||||
|
<AlbumSidebar
|
||||||
|
imageId={
|
||||||
|
this.state.activeImage != -1
|
||||||
|
? data.album.photos[this.state.activeImage].id
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
71
ui/src/Pages/AlbumPage/AlbumSidebar.js
Normal file
71
ui/src/Pages/AlbumPage/AlbumSidebar.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Query } from 'react-apollo'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
const photoQuery = gql`
|
||||||
|
query sidebarPhoto($id: ID) {
|
||||||
|
photo(id: $id) {
|
||||||
|
title
|
||||||
|
original {
|
||||||
|
path
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const RightSidebar = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
width: 500px;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 60px;
|
||||||
|
background-color: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-left: 1px solid #eee;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PreviewImage = styled.img`
|
||||||
|
width: 100%;
|
||||||
|
height: 333px;
|
||||||
|
object-fit: contain;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Name = styled.div`
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
class AlbumSidebar extends Component {
|
||||||
|
render() {
|
||||||
|
const { imageId } = this.props
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
return <RightSidebar />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RightSidebar>
|
||||||
|
<Query query={photoQuery} variables={{ id: imageId }}>
|
||||||
|
{({ loading, error, data }) => {
|
||||||
|
if (loading) return 'Loading...'
|
||||||
|
if (error) return error
|
||||||
|
|
||||||
|
const { photo } = data
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PreviewImage src={photo.original.path} />
|
||||||
|
<Name>{photo.title}</Name>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Query>
|
||||||
|
</RightSidebar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AlbumSidebar
|
||||||
@@ -6,19 +6,32 @@ export const Gallery = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const Photo = styled.img`
|
export const PhotoContainer = styled.div`
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 200px;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Photo = styled.img`
|
||||||
height: 200px;
|
height: 200px;
|
||||||
flex-grow: 1;
|
min-width: 100%;
|
||||||
|
position: relative;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const PhotoOverlay = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
${props =>
|
${props =>
|
||||||
props.active &&
|
props.active &&
|
||||||
`
|
`
|
||||||
will-change: transform;
|
border: 4px solid rgba(65, 131, 196, 0.6);
|
||||||
position: relative;
|
|
||||||
z-index: 999;
|
|
||||||
`}
|
`}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import gql from 'graphql-tag'
|
|||||||
import { Query } from 'react-apollo'
|
import { Query } from 'react-apollo'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Dimmer, Loader } from 'semantic-ui-react'
|
import { Loader } from 'semantic-ui-react'
|
||||||
|
|
||||||
const getAlbumsQuery = gql`
|
const getAlbumsQuery = gql`
|
||||||
query getMyAlbums {
|
query getMyAlbums {
|
||||||
|
|||||||
@@ -4,22 +4,15 @@ import { Route, Switch } from 'react-router-dom'
|
|||||||
import LoginPage from './Pages/LoginPage'
|
import LoginPage from './Pages/LoginPage'
|
||||||
import HomePage from './Pages/HomePage/HomePage'
|
import HomePage from './Pages/HomePage/HomePage'
|
||||||
import AlbumPage from './Pages/AlbumPage/AlbumPage'
|
import AlbumPage from './Pages/AlbumPage/AlbumPage'
|
||||||
|
import AuthorizedRoute from './AuthorizedRoute'
|
||||||
|
|
||||||
class Routes extends Component {
|
class Routes extends Component {
|
||||||
render() {
|
render() {
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
|
|
||||||
let unauthorizedRedirect = null
|
|
||||||
if (!token) {
|
|
||||||
unauthorizedRedirect = <Redirect to="/login" />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
{unauthorizedRedirect}
|
|
||||||
<Route path="/login" component={LoginPage} />
|
<Route path="/login" component={LoginPage} />
|
||||||
<Route exact path="/" component={HomePage} />
|
<AuthorizedRoute exact path="/" component={HomePage} />
|
||||||
<Route path="/album/:id" component={AlbumPage} />
|
<AuthorizedRoute path="/album/:id" component={AlbumPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user