From ae93c884ba014401c4e62ab5f34750d2ee5cd587 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Sat, 20 Jul 2019 11:48:27 +0200
Subject: [PATCH] Lazy load images and fade in

---
 ui/package.json                       |  8 +++--
 ui/src/Layout.js                      |  5 +++
 ui/src/Pages/PhotosPage/PhotosPage.js | 43 ++++++++++++++++++++++++
 ui/src/PhotoGallery.js                | 47 ++++++++++++++++++++-------
 ui/src/Routes.js                      |  2 ++
 ui/src/index.js                       |  2 +-
 ui/yarn.lock                          | 38 ++++++++++++++++++++--
 7 files changed, 127 insertions(+), 18 deletions(-)
 create mode 100644 ui/src/Pages/PhotosPage/PhotosPage.js

diff --git a/ui/package.json b/ui/package.json
index 5ef9066..248ec15 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -16,12 +16,14 @@
     "graphql-tag": "^2.10.1",
     "parcel-bundler": "^1.12.3",
     "prettier": "^1.18.2",
-    "react": "^16.8.6",
+    "react": "^0.14.0 || ^15.0.0 || ^16.0.0",
     "react-apollo": "^2.5.5",
-    "react-dom": "^16.8.6",
+    "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0",
+    "react-lazyload": "^2.6.2",
     "react-router-dom": "^5.0.1",
+    "react-spring": "^8.0.27",
+    "semantic-ui-css": "^2.4.1",
     "semantic-ui-react": "^0.87.2",
-    "semantic-ui": "^2.4.2",
     "styled-components": "^4.3.2"
   },
   "scripts": {
diff --git a/ui/src/Layout.js b/ui/src/Layout.js
index 9eba734..cbdbb3f 100644
--- a/ui/src/Layout.js
+++ b/ui/src/Layout.js
@@ -34,6 +34,7 @@ const SideButton = props => {
     width: 60px;
     height: 60px;
     margin: 10px;
+    margin-bottom: 24px;
 
     font-size: 28px;
 
@@ -77,6 +78,10 @@ class Layout extends Component {
     return (
       <Container>
         <LeftSidebar>
+          <SideButton to="/photos" exact>
+            <Icon name="image outline" />
+            <SideButtonLabel>Photos</SideButtonLabel>
+          </SideButton>
           <SideButton to="/" exact>
             <Icon name="images outline" />
             <SideButtonLabel>Albums</SideButtonLabel>
diff --git a/ui/src/Pages/PhotosPage/PhotosPage.js b/ui/src/Pages/PhotosPage/PhotosPage.js
new file mode 100644
index 0000000..3b377fd
--- /dev/null
+++ b/ui/src/Pages/PhotosPage/PhotosPage.js
@@ -0,0 +1,43 @@
+import React, { Component } from 'react'
+import Layout from '../../Layout'
+import gql from 'graphql-tag'
+import { Query } from 'react-apollo'
+import PhotoGallery from '../../PhotoGallery'
+
+const photoQuery = gql`
+  query allPhotosPage {
+    myPhotos {
+      id
+      title
+      thumbnail {
+        path
+        width
+        height
+      }
+    }
+  }
+`
+
+class PhotosPage extends Component {
+  render() {
+    return (
+      <Layout>
+        <Query query={photoQuery}>
+          {({ loading, error, data }) => {
+            if (error) return error
+
+            return (
+              <PhotoGallery
+                loading={loading}
+                title="All photos"
+                photos={data.myPhotos}
+              />
+            )
+          }}
+        </Query>
+      </Layout>
+    )
+  }
+}
+
+export default PhotosPage
diff --git a/ui/src/PhotoGallery.js b/ui/src/PhotoGallery.js
index 0d21620..9933df3 100644
--- a/ui/src/PhotoGallery.js
+++ b/ui/src/PhotoGallery.js
@@ -1,14 +1,16 @@
 import React from 'react'
 import styled from 'styled-components'
 import { Loader } from 'semantic-ui-react'
+import { useSpring, animated } from 'react-spring'
+import LazyLoad from 'react-lazyload'
 
-export const Gallery = styled.div`
+const Gallery = styled.div`
   display: flex;
   flex-wrap: wrap;
   align-items: center;
 `
 
-export const PhotoContainer = styled.div`
+const PhotoContainer = styled.div`
   flex-grow: 1;
   height: 200px;
   margin: 4px;
@@ -16,14 +18,30 @@ export const PhotoContainer = styled.div`
   position: relative;
 `
 
-export const Photo = styled.img`
-  height: 200px;
-  min-width: 100%;
-  position: relative;
-  object-fit: cover;
-`
+const Photo = photoProps => {
+  const StyledPhoto = styled(animated.img)`
+    height: 200px;
+    min-width: 100%;
+    position: relative;
+    object-fit: cover;
+  `
 
-export const PhotoOverlay = styled.div`
+  const [props, set, stop] = useSpring(() => ({ opacity: 0 }))
+
+  return (
+    <LazyLoad>
+      <StyledPhoto
+        {...photoProps}
+        style={props}
+        onLoad={() => {
+          set({ opacity: 1 })
+        }}
+      />
+    </LazyLoad>
+  )
+}
+
+const PhotoOverlay = styled.div`
   width: 100%;
   height: 100%;
   position: absolute;
@@ -37,7 +55,7 @@ export const PhotoOverlay = styled.div`
     `}
 `
 
-export const PhotoFiller = styled.div`
+const PhotoFiller = styled.div`
   height: 200px;
   flex-grow: 999999;
 `
@@ -54,10 +72,17 @@ const PhotoGallery = ({
     photoElements = photos.map((photo, index) => {
       const active = activeIndex == index
 
+      const minWidth = Math.floor(
+        (photo.thumbnail.width / photo.thumbnail.height) * 200
+      )
+
       return (
         <PhotoContainer
           key={photo.id}
-          style={{ cursor: onSelectImage && 'pointer' }}
+          style={{
+            cursor: onSelectImage ? 'pointer' : null,
+            minWidth: `${minWidth}px`,
+          }}
           onClick={() => {
             onSelectImage && onSelectImage(index)
           }}
diff --git a/ui/src/Routes.js b/ui/src/Routes.js
index 94fd2d9..928795f 100644
--- a/ui/src/Routes.js
+++ b/ui/src/Routes.js
@@ -5,6 +5,7 @@ import LoginPage from './Pages/LoginPage'
 import HomePage from './Pages/HomePage/HomePage'
 import AlbumPage from './Pages/AlbumPage/AlbumPage'
 import AuthorizedRoute from './AuthorizedRoute'
+import PhotosPage from './Pages/PhotosPage/PhotosPage'
 
 class Routes extends Component {
   render() {
@@ -13,6 +14,7 @@ class Routes extends Component {
         <Route path="/login" component={LoginPage} />
         <AuthorizedRoute exact path="/" component={HomePage} />
         <AuthorizedRoute path="/album/:id" component={AlbumPage} />
+        <AuthorizedRoute path="/photos" component={PhotosPage} />
       </Switch>
     )
   }
diff --git a/ui/src/index.js b/ui/src/index.js
index 940e624..286ea91 100644
--- a/ui/src/index.js
+++ b/ui/src/index.js
@@ -6,7 +6,7 @@ import client from './apolloClient'
 import { ApolloProvider } from 'react-apollo'
 import { BrowserRouter as Router } from 'react-router-dom'
 
-import 'semantic-ui/dist/semantic.min.css'
+import 'semantic-ui-css/semantic.min.css'
 
 const Main = () => (
   <ApolloProvider client={client}>
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 1f19290..e2836ae 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -752,6 +752,13 @@
   dependencies:
     regenerator-runtime "^0.13.2"
 
+"@babel/runtime@^7.3.1":
+  version "7.5.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
+  integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
+  dependencies:
+    regenerator-runtime "^0.13.2"
+
 "@babel/template@^7.0.0 <7.4.0":
   version "7.2.2"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907"
@@ -3529,6 +3536,11 @@ iterall@^1.2.2:
   resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
   integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==
 
+jquery@x.*:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
+  integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==
+
 js-beautify@^1.8.9:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.10.0.tgz#9753a13c858d96828658cd18ae3ca0e5783ea672"
@@ -5054,7 +5066,7 @@ promise@^7.1.1:
   dependencies:
     asap "~2.0.3"
 
-prop-types@^15.5.4, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
   version "15.7.2"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
   integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -5185,7 +5197,7 @@ react-apollo@^2.5.5:
     ts-invariant "^0.4.2"
     tslib "^1.9.3"
 
-react-dom@^16.8.6:
+"react-dom@^0.14.0 || ^15.0.0 || ^16.0.0":
   version "16.8.6"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
   integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==
@@ -5200,6 +5212,11 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
   integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
 
+react-lazyload@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/react-lazyload/-/react-lazyload-2.6.2.tgz#6a1660de6e8653632797539189d19d64e924482c"
+  integrity sha512-zbFiwI3H7W0/Qvb6T/ew2NiGe2wj+soYNW7vv5Dte1eZuJDvvyUOHo8GpYfEeWoP5x4Rree2Hwop+lCISalBwg==
+
 react-popper@^1.3.3:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6"
@@ -5241,7 +5258,15 @@ react-router@5.0.1:
     tiny-invariant "^1.0.2"
     tiny-warning "^1.0.0"
 
-react@^16.8.6:
+react-spring@^8.0.27:
+  version "8.0.27"
+  resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a"
+  integrity sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g==
+  dependencies:
+    "@babel/runtime" "^7.3.1"
+    prop-types "^15.5.8"
+
+"react@^0.14.0 || ^15.0.0 || ^16.0.0":
   version "16.8.6"
   resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
   integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==
@@ -5555,6 +5580,13 @@ scheduler@^0.13.6:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
 
+semantic-ui-css@^2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/semantic-ui-css/-/semantic-ui-css-2.4.1.tgz#f5aea39fafb787cbd905ec724272a3f9cba9004a"
+  integrity sha512-Pkp0p9oWOxlH0kODx7qFpIRYpK1T4WJOO4lNnpNPOoWKCrYsfHqYSKgk5fHfQtnWnsAKy7nLJMW02bgDWWFZFg==
+  dependencies:
+    jquery x.*
+
 semantic-ui-react@^0.87.2:
   version "0.87.2"
   resolved "https://registry.yarnpkg.com/semantic-ui-react/-/semantic-ui-react-0.87.2.tgz#b312a5a1f7395b3ff871507c59c0557fae54c984"
