diff --git a/api/src/resolvers/photos.js b/api/src/resolvers/photos.js
index 3266ac32..45fff484 100644
--- a/api/src/resolvers/photos.js
+++ b/api/src/resolvers/photos.js
@@ -10,25 +10,40 @@ function injectAt(query, index, injection) {
return query.substr(0, index) + injection + query.substr(index)
}
-const Query = {
- myAlbums: async function(root, args, ctx, info) {
- const query = cypherQuery(args, ctx, info)
+const myAlbumQuery = function(args, ctx, info) {
+ const query = cypherQuery(args, ctx, info)
- const whereSplit = query[0].indexOf('RETURN')
+ const whereSplit = query[0].indexOf('RETURN')
+
+ query[0] = injectAt(
+ query[0],
+ whereSplit,
+ `MATCH (u:User { id: {userid} }) WHERE (u)-[:OWNS]->(album) `
+ )
+ query[1].userid = ctx.user.id
+
+ const addIDSplitIndex = query[0].indexOf('album_photos {')
+
+ if (addIDSplitIndex != -1) {
+ const addIDSplit = addIDSplitIndex + 14
+
+ console.log('ID SPLIT', query[0])
query[0] = injectAt(
query[0],
- whereSplit,
- `MATCH (u:User { id: {userid} }) WHERE (u)-[:OWNS]->(album) `
+ addIDSplit,
+ query[0].indexOf('album_photos {}') == -1 ? ` .id, ` : ` .id `
)
- query[1].userid = ctx.user.id
+ }
+
+ return query
+}
+
+const Query = {
+ async myAlbums(root, args, ctx, info) {
+ let query = myAlbumQuery(args, ctx, info)
console.log(query)
- const addIDSplit = query[0].indexOf('album_photos {') + 14
-
- console.log('ID SPLIT', query[0].substr(0, addIDSplit))
- query[0] = injectAt(query[0], addIDSplit, `.id,`)
-
const session = ctx.driver.session()
const result = await session.run(...query)
@@ -37,16 +52,32 @@ const Query = {
return result.records.map(record => record.get('album'))
},
+ async album(root, args, ctx, info) {
+ const session = ctx.driver.session()
+
+ let query = myAlbumQuery(args, ctx, info)
+
+ const whereSplit = query[0].indexOf('RETURN')
+
+ query[0] = injectAt(query[0], whereSplit, ` AND album.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('album')
+ },
}
-const Photo = {
- async original(root, args, ctx, info) {
- const imgPath = path.resolve(
- config.cachePath,
- 'images',
- root.id,
- 'original.jpg'
- )
+function photoResolver(image) {
+ return async (root, args, ctx, info) => {
+ const imgPath = path.resolve(config.cachePath, 'images', root.id, image)
if (!(await fs.exists(imgPath))) {
await ctx.scanner.processImage(root.id)
@@ -54,18 +85,16 @@ const Photo = {
const { width, height } = await imageSize(imgPath)
return {
- path: `${ctx.endpoint}/images/${root.id}/original.jpg`,
+ path: `${ctx.endpoint}/images/${root.id}/${image}`,
width,
height,
}
- },
- async thumbnail(root, args, ctx, info) {
- return {
- path: `${ctx.endpoint}/images/${root.id}/thumbnail.jpg`,
- width: 120,
- height: 240,
- }
- },
+ }
+}
+
+const Photo = {
+ original: photoResolver('original.jpg'),
+ thumbnail: photoResolver('thumbnail.jpg'),
}
export default {
diff --git a/api/src/schema.graphql b/api/src/schema.graphql
index 5e2c2169..e549fad6 100644
--- a/api/src/schema.graphql
+++ b/api/src/schema.graphql
@@ -74,4 +74,5 @@ type Query {
siteInfo: SiteInfo
myAlbums: [Album] @isAuthenticated
+ album(id: ID): Album @isAuthenticated
}
diff --git a/ui/package.json b/ui/package.json
index eb0d97bc..5ef9066a 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -21,6 +21,7 @@
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.1",
"semantic-ui-react": "^0.87.2",
+ "semantic-ui": "^2.4.2",
"styled-components": "^4.3.2"
},
"scripts": {
@@ -33,6 +34,7 @@
"devDependencies": {
"@babel/core": "^7.5.0",
"husky": "^3.0.0",
+ "isarray": "^2.0.5",
"lint-staged": "^9.1.0"
},
"husky": {
diff --git a/ui/src/Albums.js b/ui/src/Albums.js
new file mode 100644
index 00000000..418bcd9c
--- /dev/null
+++ b/ui/src/Albums.js
@@ -0,0 +1,84 @@
+import React, { Component } from 'react'
+import gql from 'graphql-tag'
+import { Query } from 'react-apollo'
+import styled from 'styled-components'
+import { Link } from 'react-router-dom'
+import { Dimmer, Loader } from 'semantic-ui-react'
+
+const getAlbumsQuery = gql`
+ query getMyAlbums {
+ myAlbums {
+ id
+ title
+ photos {
+ thumbnail {
+ path
+ }
+ }
+ }
+ }
+`
+
+const AlbumBoxLink = styled(Link)`
+ width: 240px;
+ height: 240px;
+ display: inline-block;
+ text-align: center;
+ color: #222;
+`
+
+const AlbumBoxImage = styled.div`
+ width: 220px;
+ height: 220px;
+ margin: auto;
+ border-radius: 4%;
+ background-image: url('${props => props.image}');
+ background-color: #eee;
+ background-size: cover;
+ background-position: center;
+`
+
+class Albums extends Component {
+ render() {
+ return (
+
+
+ {({ loading, error, data }) => {
+ // if (loading) return
+ if (error) return Error {error.message}
+
+ let albums
+
+ if (data && data.myAlbums) {
+ albums = data.myAlbums.map(album => (
+
+
+ {album.title}
+
+ ))
+ } else {
+ albums = []
+ for (let i = 0; i < 8; i++) {
+ albums.push(
+
+
+
+ )
+ }
+ }
+
+ return (
+
+ {' '}
+ Loading images
+ {albums}
+
+ )
+ }}
+
+
+ )
+ }
+}
+
+export default Albums
diff --git a/ui/src/Pages/AlbumPage.js b/ui/src/Pages/AlbumPage.js
new file mode 100644
index 00000000..bc6d719e
--- /dev/null
+++ b/ui/src/Pages/AlbumPage.js
@@ -0,0 +1,230 @@
+import React, { Component } from 'react'
+import gql from 'graphql-tag'
+import { Query } from 'react-apollo'
+import styled from 'styled-components'
+import { useSpring, animated } from 'react-spring'
+
+const albumQuery = gql`
+ query albumQuery($id: ID) {
+ album(id: $id) {
+ title
+ photos {
+ id
+ thumbnail {
+ path
+ width
+ height
+ }
+ }
+ }
+ }
+`
+
+const Gallery = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+`
+
+const Photo = styled.img`
+ margin: 4px;
+ background-color: #eee;
+ display: inline-block;
+ height: 200px;
+ flex-grow: 1;
+ object-fit: cover;
+
+ &:nth-last-child(6) ~ img {
+ flex-grow: 0;
+ }
+
+ ${props =>
+ props.active &&
+ `
+ will-change: transform;
+ position: relative;
+ z-index: 999;
+ `}
+`
+
+const Dimmer = ({ onClick, active }) => {
+ const [props, set, stop] = useSpring(() => ({ opacity: 0 }))
+
+ set({
+ opacity: active ? 1 : 0,
+ })
+
+ const AnimatedDimmer = styled(animated.div)`
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ background-color: black;
+ margin: 0;
+ z-index: 10;
+ `
+
+ return (
+
+ )
+}
+
+class AlbumPage extends Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ activeImage: -1,
+ }
+
+ this.setActiveImage = this.setActiveImage.bind(this)
+ this.animateImage = this.animateImage.bind(this)
+
+ this.photoAmount = 1
+ this.previousActive = false
+
+ this.scrollEvent = () => {
+ if (this.state.activeImage != -1) {
+ this.dismissPopup()
+ }
+ }
+
+ this.keyUpEvent = e => {
+ const activeImage = this.state.activeImage
+ if (activeImage != -1) {
+ if (e.key == 'ArrowRight') {
+ this.setActiveImage((activeImage + 1) % this.photoAmount)
+ }
+
+ if (e.key == 'ArrowLeft') {
+ if (activeImage <= 0) {
+ this.setActiveImage(this.photoAmount - 1)
+ } else {
+ this.setActiveImage(activeImage - 1)
+ }
+ }
+ }
+ }
+ }
+
+ componentDidMount() {
+ document.addEventListener('scroll', this.scrollEvent)
+ document.addEventListener('keyup', this.keyUpEvent)
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('scroll', this.scrollEvent)
+ document.removeEventListener('keyup', this.keyUpEvent)
+ }
+
+ setActiveImage(index) {
+ this.previousActive = this.state.activeImage != -1
+ this.setState({
+ activeImage: index,
+ })
+ }
+
+ dismissPopup() {
+ this.previousActive = true
+ this.setState({
+ activeImage: -1,
+ })
+ }
+
+ animateImage(target) {
+ const margin = 32
+
+ const windowWidth = window.innerWidth
+ const windowHeight = window.innerHeight
+
+ const viewportWidth = windowWidth - margin * 2
+ const viewportHeight = windowHeight - margin * 2
+
+ const { naturalWidth, naturalHeight } = target
+ const { width, height, top, left } = target.getBoundingClientRect()
+
+ const scaleX = Math.min(naturalWidth, viewportWidth) / width
+ const scaleY = Math.min(naturalHeight, viewportHeight) / height
+ const scale = Math.min(scaleX, scaleY)
+
+ const translateX = (-left + (windowWidth - width) / 2) / scale
+ const translateY = (-top + (windowHeight - height) / 2) / scale
+
+ if (!this.previousActive) target.style.transition = `transform 0.4s`
+
+ requestAnimationFrame(() => {
+ target.style.transform = `scale(${scale})
+ translate3d(${translateX}px, ${translateY}px, 0)`
+ })
+ }
+
+ render() {
+ const albumId = this.props.match.params.id
+
+ // let dimmer = this.state.activeImage != -1 && (
+ // this.setActiveImage(-1)} />
+ // )
+
+ return (
+
+
this.setActiveImage(-1)}
+ active={this.state.activeImage != -1}
+ />
+
+ {({ loading, error, data }) => {
+ if (error) return Error
+ if (loading) return Loading
+
+ this.photoAmount = data.album.photos.length
+
+ const { activeImage } = this.state
+
+ const photos = data.album.photos.map((photo, index) => {
+ const active = index == activeImage
+ let ref = null
+ let style = {}
+
+ if (active) {
+ ref = target => {
+ if (target == null) return
+ this.animateImage(target)
+ }
+ } else {
+ style.transform = null
+ }
+
+ return (
+ {
+ requestAnimationFrame(() => {
+ if (activeImage != index) {
+ this.setActiveImage(index)
+ } else {
+ this.dismissPopup()
+ }
+ })
+ }}
+ style={style}
+ src={photo.thumbnail.path}
+ >
+ )
+ })
+
+ return {photos}
+ }}
+
+
+ )
+ }
+}
+
+export default AlbumPage
diff --git a/ui/src/Pages/HomePage.js b/ui/src/Pages/HomePage.js
index 4fefac25..817e78c0 100644
--- a/ui/src/Pages/HomePage.js
+++ b/ui/src/Pages/HomePage.js
@@ -1,10 +1,12 @@
import React, { Component } from 'react'
+import Albums from '../Albums'
class HomePage extends Component {
render() {
return (
)
}
diff --git a/ui/src/Pages/LoginPage.js b/ui/src/Pages/LoginPage.js
index 72590c39..3cf68e44 100644
--- a/ui/src/Pages/LoginPage.js
+++ b/ui/src/Pages/LoginPage.js
@@ -14,6 +14,13 @@ const authorizeMutation = gql`
}
`
+function setCookie(cname, cvalue, exdays) {
+ var d = new Date()
+ d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)
+ var expires = 'expires=' + d.toUTCString()
+ document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/'
+}
+
class LoginPage extends Component {
constructor(props) {
super(props)
@@ -57,6 +64,7 @@ class LoginPage extends Component {
if (success) {
localStorage.setItem('token', token)
+ setCookie('token', token, 360)
window.location = '/'
}
}}
diff --git a/ui/src/Routes.js b/ui/src/Routes.js
index 18f498f9..5937b832 100644
--- a/ui/src/Routes.js
+++ b/ui/src/Routes.js
@@ -3,13 +3,15 @@ import { Route, Switch } from 'react-router-dom'
import LoginPage from './Pages/LoginPage'
import HomePage from './Pages/HomePage'
+import AlbumPage from './Pages/AlbumPage'
class Routes extends Component {
render() {
return (
-
+
+
)
}
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 39d01c1c..1f19290e 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -3489,6 +3489,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+isarray@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+ integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"