From 705aaeba0ea4acc67a78b3410c27dc2fb23e951a Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Thu, 18 Jul 2019 17:40:54 +0200
Subject: [PATCH] Working on animations

---
 api/src/resolvers/photos.js |  87 +++++++++-----
 api/src/schema.graphql      |   1 +
 ui/package.json             |   2 +
 ui/src/Albums.js            |  84 +++++++++++++
 ui/src/Pages/AlbumPage.js   | 230 ++++++++++++++++++++++++++++++++++++
 ui/src/Pages/HomePage.js    |   2 +
 ui/src/Pages/LoginPage.js   |   8 ++
 ui/src/Routes.js            |   4 +-
 ui/yarn.lock                |   5 +
 9 files changed, 393 insertions(+), 30 deletions(-)
 create mode 100644 ui/src/Albums.js
 create mode 100644 ui/src/Pages/AlbumPage.js

diff --git a/api/src/resolvers/photos.js b/api/src/resolvers/photos.js
index 3266ac3..45fff48 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 5e2c216..e549fad 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 eb0d97b..5ef9066 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 0000000..418bcd9
--- /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 (
+      <div style={{ position: 'relative', minHeight: '500px' }}>
+        <Query query={getAlbumsQuery}>
+          {({ loading, error, data }) => {
+            // if (loading) return <Loader active />
+            if (error) return <div>Error {error.message}</div>
+
+            let albums
+
+            if (data && data.myAlbums) {
+              albums = data.myAlbums.map(album => (
+                <AlbumBoxLink key={album.id} to={`/album/${album.id}`}>
+                  <AlbumBoxImage image={album.photos[0].thumbnail.path} />
+                  <p>{album.title}</p>
+                </AlbumBoxLink>
+              ))
+            } else {
+              albums = []
+              for (let i = 0; i < 8; i++) {
+                albums.push(
+                  <AlbumBoxLink key={i} to="#">
+                    <AlbumBoxImage />
+                  </AlbumBoxLink>
+                )
+              }
+            }
+
+            return (
+              <div>
+                {' '}
+                <Loader active={loading}>Loading images</Loader>
+                {albums}
+              </div>
+            )
+          }}
+        </Query>
+      </div>
+    )
+  }
+}
+
+export default Albums
diff --git a/ui/src/Pages/AlbumPage.js b/ui/src/Pages/AlbumPage.js
new file mode 100644
index 0000000..bc6d719
--- /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 (
+    <AnimatedDimmer
+      onClick={onClick}
+      style={{
+        ...props,
+        pointerEvents: active ? 'auto' : 'none',
+      }}
+    />
+  )
+}
+
+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 && (
+    //   <Dimmer onClick={() => this.setActiveImage(-1)} />
+    // )
+
+    return (
+      <div>
+        <Dimmer
+          onClick={() => this.setActiveImage(-1)}
+          active={this.state.activeImage != -1}
+        />
+        <Query query={albumQuery} variables={{ id: albumId }}>
+          {({ loading, error, data }) => {
+            if (error) return <div>Error</div>
+            if (loading) return <div>Loading</div>
+
+            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 (
+                <Photo
+                  ref={ref}
+                  active={active}
+                  key={photo.id}
+                  onClick={() => {
+                    requestAnimationFrame(() => {
+                      if (activeImage != index) {
+                        this.setActiveImage(index)
+                      } else {
+                        this.dismissPopup()
+                      }
+                    })
+                  }}
+                  style={style}
+                  src={photo.thumbnail.path}
+                ></Photo>
+              )
+            })
+
+            return <Gallery>{photos}</Gallery>
+          }}
+        </Query>
+      </div>
+    )
+  }
+}
+
+export default AlbumPage
diff --git a/ui/src/Pages/HomePage.js b/ui/src/Pages/HomePage.js
index 4fefac2..817e78c 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 (
       <div>
         <h1>Home</h1>
+        <Albums />
       </div>
     )
   }
diff --git a/ui/src/Pages/LoginPage.js b/ui/src/Pages/LoginPage.js
index 72590c3..3cf68e4 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 18f498f..5937b83 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 (
       <Switch>
         <Route path="/login" component={LoginPage} />
-        <Route path="/" component={HomePage} />
+        <Route exact path="/" component={HomePage} />
+        <Route path="/album/:id" component={AlbumPage} />
       </Switch>
     )
   }
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 39d01c1..1f19290 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"
