From 705aaeba0ea4acc67a78b3410c27dc2fb23e951a Mon Sep 17 00:00:00 2001 From: viktorstrate 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 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 (

Home

+
) } 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"