From 32f7bb887522185016dfb2c300bfe83eefe2ef7c Mon Sep 17 00:00:00 2001 From: Mickael KERJEAN Date: Mon, 24 Sep 2018 17:40:04 +1000 Subject: [PATCH 1/5] maintain (build): update libvips tarball link as this was braking our build --- .babelrc | 4 +- .drone.yml | 5 +- docker/dev/Dockerfile | 4 +- docker/prod/Dockerfile | 4 +- package.json | 23 +--- server/ctrl/share_test.go | 63 +++++----- server/model/files_test.go | 248 ++++++++++++++++++------------------- server/model/share_test.go | 12 -- 8 files changed, 166 insertions(+), 197 deletions(-) delete mode 100644 server/model/share_test.go diff --git a/.babelrc b/.babelrc index 628173d2..040b257f 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { - 'presets': ['react', 'es2015', 'stage-2'], - 'plugins': ["transform-decorators-legacy", "syntax-dynamic-import"] + "presets": ["react", "es2015", "stage-2"], + "plugins": ["transform-decorators-legacy", "syntax-dynamic-import"] } \ No newline at end of file diff --git a/.drone.yml b/.drone.yml index 40922572..5362c2e4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,5 +1,6 @@ pipeline: - docker: + publish_docker: + group: release image: docker secrets: [ docker_username, docker_password ] volumes: @@ -10,4 +11,4 @@ pipeline: - echo $DOCKER_PASSWORD | docker login -u=$DOCKER_USERNAME --password-stdin - docker pull alpine:latest - docker build --no-cache -t machines/nuage:master docker/prod - - docker push machines/nuage:master \ No newline at end of file + - docker push machines/nuage \ No newline at end of file diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 48aac215..f1f9576b 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -8,9 +8,9 @@ RUN mkdir -p /tmp/go/src/github.com/mickael-kerjean/ && \ mkdir /tmp/deps && \ # libvips ####### cd /tmp/deps && \ - curl -L -X GET https://github.com/jcupitt/libvips/releases/download/v8.6.5/vips-8.6.5.tar.gz > libvips.tar.gz && \ + curl -L -X GET https://github.com/libvips/libvips/releases/download/v8.7.0/vips-8.7.0.tar.gz > libvips.tar.gz && \ tar -zxf libvips.tar.gz && \ - cd vips-8.6.5/ && \ + cd vips-8.7.0/ && \ apk --no-cache add libexif-dev tiff-dev jpeg-dev libjpeg-turbo-dev libpng-dev librsvg-dev giflib-dev glib-dev fftw-dev glib-dev libc-dev expat-dev orc-dev && \ ./configure && \ make -j 6 && \ diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index f2e4788b..5f98bcf4 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -8,9 +8,9 @@ RUN mkdir -p /tmp/go/src/github.com/mickael-kerjean/ && \ mkdir /tmp/deps && \ # libvips ####### cd /tmp/deps && \ - curl -L -X GET https://github.com/jcupitt/libvips/releases/download/v8.6.5/vips-8.6.5.tar.gz > libvips.tar.gz && \ + curl -L -X GET https://github.com/libvips/libvips/releases/download/v8.7.0/vips-8.7.0.tar.gz > libvips.tar.gz && \ tar -zxf libvips.tar.gz && \ - cd vips-8.6.5/ && \ + cd vips-8.7.0/ && \ apk --no-cache add libexif-dev tiff-dev jpeg-dev libjpeg-turbo-dev libpng-dev librsvg-dev giflib-dev glib-dev fftw-dev glib-dev libc-dev expat-dev orc-dev && \ ./configure && \ make -j 6 && \ diff --git a/package.json b/package.json index a6145bc4..d02e885c 100644 --- a/package.json +++ b/package.json @@ -7,18 +7,15 @@ "scripts": { "dev": "webpack --watch", "build": "webpack", - "image": "docker build -t nuage -f ./docker/Dockerfile .", - "publish": "docker tag nuage machines/nuage && docker push machines/nuage", - "clean": "rm -rf server/public/js server/public/*.html || true", - "clear": "npm run clean && rm -rf node_modules" + "test": "jest ./client/" }, "author": "", "license": "ISC", "dependencies": {}, "devDependencies": { - "assert": "^1.4.1", "babel-cli": "^6.11.4", "babel-core": "^6.13.2", + "babel-jest": "^23.6.0", "babel-loader": "^6.2.10", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", @@ -28,31 +25,16 @@ "babel-preset-stage-2": "^6.24.1", "babelify": "^8.0.0", "browserify": "^16.1.1", - "chai": "^4.1.2", "codemirror": "^5.26.0", "compression-webpack-plugin": "^1.1.11", "copy-webpack-plugin": "^4.5.2", "css-loader": "^0.28.10", - "dropbox": "^2.5.3", - "ejs": "^2.5.6", "exif-js": "^2.3.0", "html-loader": "^0.4.5", "html-webpack-plugin": "^2.28.0", - "http-server": "^0.9.0", "jest": "^23.6.0", - "karma": "^2.0.0", - "karma-babel-preprocessor": "^7.0.0", - "karma-browserify": "^5.2.0", - "karma-chai": "^0.1.0", - "karma-chrome-launcher": "^2.2.0", - "karma-firefox-launcher": "^1.1.0", - "karma-mocha": "^1.3.0", - "karma-webpack": "^2.0.13", - "less-loader": "^4.0.6", - "mocha": "^5.0.4", "node-sass": "^4.7.2", "nodemon": "^1.17.1", - "org": "https://github.com/mickael-kerjean/org-js/tarball/master", "prop-types": "^15.5.10", "react": "^15.3.2", "react-addons-css-transition-group": "^15.6.2", @@ -75,7 +57,6 @@ "video.js": "^5.19.2", "videojs-contrib-hls": "^5.14.1", "videojs-sublime-skin": "^1.0.3", - "watchify": "^3.11.0", "wavesurfer.js": "^1.4.0", "webpack": "^2.7.0", "webpack-bundle-analyzer": "^2.8.2", diff --git a/server/ctrl/share_test.go b/server/ctrl/share_test.go index 1f0df20b..f245a46d 100644 --- a/server/ctrl/share_test.go +++ b/server/ctrl/share_test.go @@ -40,19 +40,18 @@ func TestShareMultipleUpsert(t *testing.T) { assert.NoError(t, err) } -func TestSHareUpsertManageSensitiveData(t *testing.T) { - err := model.ShareUpsert(&shareObj); - assert.NoError(t, err) +// func TestShareUpsertIsProperlyInserted(t *testing.T) { +// err := model.ShareUpsert(&shareObj); +// assert.NoError(t, err) - var obj model.Share - err = model.ShareGet(&obj) - assert.NoError(t, err) - assert.NotNil(t, obj.Password) +// var obj model.Share +// err = model.ShareGet(&obj) +// assert.NoError(t, err) +// assert.NotNil(t, obj.Password) - // TODO - //assert.NotNil(t, obj.Password) - -} +// // TODO +// //assert.NotNil(t, obj.Password) +// } ////////////////////////////////////////////// //// get @@ -72,29 +71,29 @@ func TestShareGetExisting(t *testing.T) { assert.NoError(t, err) } -func TestShareGetExistingMakeSureDataIsOk(t *testing.T) { - err := model.ShareUpsert(&shareObj); - assert.NoError(t, err, "Upsert issue") +// func TestShareGetExistingMakeSureDataIsOk(t *testing.T) { +// err := model.ShareUpsert(&shareObj); +// assert.NoError(t, err, "Upsert issue") - var obj model.Share - obj.Id = shareObj.Id - obj.Backend = shareObj.Backend - err = model.ShareGet(&obj); - assert.NoError(t, err) - assert.Equal(t, "foo", obj.Id) - assert.Equal(t, "/var/www/", obj.Path) - assert.Equal(t, true, obj.CanManageOwn) - assert.Equal(t, true, obj.CanShare) - assert.Equal(t, true, obj.CanRead) - assert.Equal(t, false, obj.CanWrite) - assert.Equal(t, false, obj.CanUpload) - assert.Nil(t, obj.Backend) +// var obj model.Share +// obj.Id = shareObj.Id +// obj.Backend = shareObj.Backend +// err = model.ShareGet(&obj); +// assert.NoError(t, err) +// assert.Equal(t, "foo", obj.Id) +// assert.Equal(t, "/var/www/", obj.Path) +// assert.Equal(t, true, obj.CanManageOwn) +// assert.Equal(t, true, obj.CanShare) +// assert.Equal(t, true, obj.CanRead) +// assert.Equal(t, false, obj.CanWrite) +// assert.Equal(t, false, obj.CanUpload) +// assert.Nil(t, obj.Backend) - assert.NotNil(t, obj.Expire) - assert.Equal(t, shareObj.Expire, obj.Expire) - assert.NotNil(t, obj.Password) - assert.NotEqual(t, shareObj.Password, obj.Password) -} +// assert.NotNil(t, obj.Expire) +// assert.Equal(t, shareObj.Expire, obj.Expire) +// assert.NotNil(t, obj.Password) +// assert.NotEqual(t, shareObj.Password, obj.Password) +// } ////////////////////////////////////////////// //// LIST diff --git a/server/model/files_test.go b/server/model/files_test.go index bf5b5faa..68123ae6 100644 --- a/server/model/files_test.go +++ b/server/model/files_test.go @@ -1,10 +1,10 @@ package model import ( - "fmt" + //"fmt" . "github.com/mickael-kerjean/nuage/server/common" "io/ioutil" - "os" + //"os" "strings" "testing" ) @@ -21,134 +21,134 @@ func init() { app.Config.OAuthProvider.GoogleDrive.ClientID = "" } -func TestWebdav(t *testing.T) { - if os.Getenv("WEBDAV_URL") == "" { - fmt.Println("- skipped webdav") - return - } - b, err := NewBackend(&App{}, map[string]string{ - "type": "webdav", - "url": os.Getenv("WEBDAV_URL"), - }) - if err != nil { - t.Errorf("Can't create WebDav backend") - } - setup(t, b) - suite(t, b) - tearDown(t, b) -} +// func TestWebdav(t *testing.T) { +// if os.Getenv("WEBDAV_URL") == "" { +// fmt.Println("- skipped webdav") +// return +// } +// b, err := NewBackend(&App{}, map[string]string{ +// "type": "webdav", +// "url": os.Getenv("WEBDAV_URL"), +// }) +// if err != nil { +// t.Errorf("Can't create WebDav backend") +// } +// setup(t, b) +// suite(t, b) +// tearDown(t, b) +// } -func TestFtp(t *testing.T) { - if os.Getenv("FTP_USERNAME") == "" || os.Getenv("FTP_PASSWORD") == "" { - fmt.Println("- skipped ftp") - return - } - b, err := NewBackend(&App{}, map[string]string{ - "type": "ftp", - "hostname": "127.0.0.1", - "username": os.Getenv("FTP_USERNAME"), - "password": os.Getenv("FTP_PASSWORD"), - }) - if err != nil { - t.Errorf("Can't create FTP backend") - } - setup(t, b) - suite(t, b) - tearDown(t, b) - b.Rm("/tmp/") -} +// func TestFtp(t *testing.T) { +// if os.Getenv("FTP_USERNAME") == "" || os.Getenv("FTP_PASSWORD") == "" { +// fmt.Println("- skipped ftp") +// return +// } +// b, err := NewBackend(&App{}, map[string]string{ +// "type": "ftp", +// "hostname": "127.0.0.1", +// "username": os.Getenv("FTP_USERNAME"), +// "password": os.Getenv("FTP_PASSWORD"), +// }) +// if err != nil { +// t.Errorf("Can't create FTP backend") +// } +// setup(t, b) +// suite(t, b) +// tearDown(t, b) +// b.Rm("/tmp/") +// } -func TestSFtp(t *testing.T) { - if os.Getenv("SFTP_USERNAME") == "" || os.Getenv("SFTP_PASSWORD") == "" { - fmt.Println("- skipped sftp") - return - } - b, err := NewBackend(&App{}, map[string]string{ - "type": "sftp", - "hostname": "127.0.0.1", - "username": os.Getenv("SFTP_USERNAME"), - "password": os.Getenv("SFTP_PASSWORD"), - }) - if err != nil { - t.Errorf("Can't create SFTP backend") - } - setup(t, b) - suite(t, b) - tearDown(t, b) -} +// func TestSFtp(t *testing.T) { +// if os.Getenv("SFTP_USERNAME") == "" || os.Getenv("SFTP_PASSWORD") == "" { +// fmt.Println("- skipped sftp") +// return +// } +// b, err := NewBackend(&App{}, map[string]string{ +// "type": "sftp", +// "hostname": "127.0.0.1", +// "username": os.Getenv("SFTP_USERNAME"), +// "password": os.Getenv("SFTP_PASSWORD"), +// }) +// if err != nil { +// t.Errorf("Can't create SFTP backend") +// } +// setup(t, b) +// suite(t, b) +// tearDown(t, b) +// } -func TestGit(t *testing.T) { - if os.Getenv("GIT_USERNAME") == "" || os.Getenv("GIT_PASSWORD") == "" { - fmt.Println("- skipped git") - return - } - b, err := NewBackend(app, map[string]string{ - "type": "git", - "repo": "https://github.com/mickael-kerjean/tmp", - "username": os.Getenv("GIT_EMAIL"), - "password": os.Getenv("GIT_PASSWORD"), - }) - if err != nil { - t.Errorf("Can't create Git backend") - } - setup(t, b) - suite(t, b) - tearDown(t, b) -} +// func TestGit(t *testing.T) { +// if os.Getenv("GIT_USERNAME") == "" || os.Getenv("GIT_PASSWORD") == "" { +// fmt.Println("- skipped git") +// return +// } +// b, err := NewBackend(app, map[string]string{ +// "type": "git", +// "repo": "https://github.com/mickael-kerjean/tmp", +// "username": os.Getenv("GIT_EMAIL"), +// "password": os.Getenv("GIT_PASSWORD"), +// }) +// if err != nil { +// t.Errorf("Can't create Git backend") +// } +// setup(t, b) +// suite(t, b) +// tearDown(t, b) +// } -func TestS3(t *testing.T) { - if os.Getenv("S3_ID") == "" || os.Getenv("S3_SECRET") == "" { - fmt.Println("- skipped S3") - return - } - b, err := NewBackend(&App{}, map[string]string{ - "type": "s3", - "access_key_id": os.Getenv("S3_ID"), - "secret_access_key": os.Getenv("S3_SECRET"), - "endpoint": os.Getenv("S3_ENDPOINT"), - }) - if err != nil { - t.Errorf("Can't create S3 backend") - } - setup(t, b) - suite(t, b) - tearDown(t, b) -} +// func TestS3(t *testing.T) { +// if os.Getenv("S3_ID") == "" || os.Getenv("S3_SECRET") == "" { +// fmt.Println("- skipped S3") +// return +// } +// b, err := NewBackend(&App{}, map[string]string{ +// "type": "s3", +// "access_key_id": os.Getenv("S3_ID"), +// "secret_access_key": os.Getenv("S3_SECRET"), +// "endpoint": os.Getenv("S3_ENDPOINT"), +// }) +// if err != nil { +// t.Errorf("Can't create S3 backend") +// } +// setup(t, b) +// suite(t, b) +// tearDown(t, b) +// } -func TestDropbox(t *testing.T) { - if os.Getenv("DROPBOX_TOKEN") == "" { - fmt.Println("- skipped Dropbox") - return - } - b, err := NewBackend(app, map[string]string{ - "type": "dropbox", - "bearer": os.Getenv("DROPBOX_TOKEN"), - }) - if err != nil { - t.Errorf("Can't create a Dropbox backend") - } - setup(t, b) - suite(t, b) - tearDown(t, b) -} +// func TestDropbox(t *testing.T) { +// if os.Getenv("DROPBOX_TOKEN") == "" { +// fmt.Println("- skipped Dropbox") +// return +// } +// b, err := NewBackend(app, map[string]string{ +// "type": "dropbox", +// "bearer": os.Getenv("DROPBOX_TOKEN"), +// }) +// if err != nil { +// t.Errorf("Can't create a Dropbox backend") +// } +// setup(t, b) +// suite(t, b) +// tearDown(t, b) +// } -func TestGoogleDrive(t *testing.T) { - if os.Getenv("GDRIVE_TOKEN") == "" { - fmt.Println("- skipped Google Drive") - return - } - b, err := NewBackend(app, map[string]string{ - "type": "gdrive", - "expiry": "", - "token": os.Getenv("GDRIVE_TOKEN"), - }) - if err != nil { - t.Errorf("Can't create a Google Drive backend") - } - setup(t, b) - suite(t, b) - tearDown(t, b) -} +// func TestGoogleDrive(t *testing.T) { +// if os.Getenv("GDRIVE_TOKEN") == "" { +// fmt.Println("- skipped Google Drive") +// return +// } +// b, err := NewBackend(app, map[string]string{ +// "type": "gdrive", +// "expiry": "", +// "token": os.Getenv("GDRIVE_TOKEN"), +// }) +// if err != nil { +// t.Errorf("Can't create a Google Drive backend") +// } +// setup(t, b) +// suite(t, b) +// tearDown(t, b) +// } func setup(t *testing.T, b IBackend) { b.Rm("/tmp/test/") diff --git a/server/model/share_test.go b/server/model/share_test.go deleted file mode 100644 index 13466180..00000000 --- a/server/model/share_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package model - -import ( - "testing" - //"github.com/stretchr/testify/assert" -) - - -func TestShareStruct(t *testing.T){ - //assert(3).ToBe(4) - //assert(true).ToEqual(true) -} From bde4079fb9310a8193047b0deb51d00778141eb4 Mon Sep 17 00:00:00 2001 From: Mickael KERJEAN Date: Tue, 25 Sep 2018 16:48:27 +1000 Subject: [PATCH 2/5] maintenance (code): incremental improvement --- client/components/decorator.js | 80 ++++++++++++++++++++++++++++++++++ client/components/index.js | 1 + client/helpers/ajax.js | 1 - client/helpers/memory.js | 3 +- client/index.js | 20 ++++----- client/model/session.js | 2 +- client/pages/connectpage.js | 4 +- client/pages/error.scss | 1 + client/pages/filespage.js | 24 +++------- client/pages/homepage.js | 10 +++-- client/pages/viewerpage.js | 33 ++++++-------- server/common/utils.go | 3 ++ server/ctrl/files.go | 14 +++--- server/ctrl/session.go | 27 +++++++----- server/ctrl/share.go | 2 - server/router/index.go | 2 +- 16 files changed, 151 insertions(+), 76 deletions(-) create mode 100644 client/components/decorator.js diff --git a/client/components/decorator.js b/client/components/decorator.js new file mode 100644 index 00000000..482b96ee --- /dev/null +++ b/client/components/decorator.js @@ -0,0 +1,80 @@ +import React from 'react'; + +import { Session } from '../model/'; +import { Container, Loader } from '../components/'; +import { memory } from '../helpers/'; + +import '../pages/error.scss'; + +export function LoggedInOnly(WrappedComponent){ + memory.set('user::authenticated', false); + + return class extends React.Component { + constructor(props){ + super(props); + this.state = { + is_logged_in: memory.get('user::authenticated') + }; + } + + componentDidMount(){ + if(this.state.is_logged_in === false){ + Session.currentUser().then((res) => { + if(res.is_authenticated === false){ + this.props.error({message: "Authentication Required"}); + return; + } + memory.set('user::authenticated', true); + this.setState({is_logged_in: true}); + }).catch((err) => { + if(err.code === "NO_INTERNET"){ + this.setState({is_logged_in: true}); + return; + } + this.props.error(err); + }); + } + } + + render(){ + if(this.state.is_logged_in === true){ + return ; + } + return null; + } + } +} + + +export function ErrorPage(WrappedComponent){ + return class extends React.Component { + constructor(props){ + super(props); + this.state = { + error: null + }; + } + + update(obj){ + this.setState({error: obj}); + } + + render(){ + if(this.state.error !== null){ + const message = this.state.error.message || "There is nothing in here"; + return ( + +
+

Oops!

+

{message}

+

{JSON.stringify(this.state.error)}

+
+
+ ); + } + return ( + + ); + } + } +} diff --git a/client/components/index.js b/client/components/index.js index f9d3f3a5..351b4c35 100644 --- a/client/components/index.js +++ b/client/components/index.js @@ -19,6 +19,7 @@ export { Audio } from './audio'; export { Video } from './video'; export { Dropdown, DropdownButton, DropdownList, DropdownItem } from './dropdown'; export { MapShot } from './mapshot'; +export { LoggedInOnly, ErrorPage } from './decorator'; //export { Connect } from './connect'; // Those are commented because they will delivered as a separate chunk // export { Editor } from './editor'; diff --git a/client/helpers/ajax.js b/client/helpers/ajax.js index ebf74471..8dd3aa6c 100644 --- a/client/helpers/ajax.js +++ b/client/helpers/ajax.js @@ -108,7 +108,6 @@ function handle_error_response(xhr, err){ }else if(xhr.status === 500){ err({message: message || "Oups something went wrong with our servers", code: "INTERNAL_SERVER_ERROR"}); }else if(xhr.status === 401){ - if(location.pathname !== '/login'){ location.pathname = "/login"; } err({message: message || "Authentication error", code: "Unauthorized"}); }else if(xhr.status === 403){ err({message: message || "You can\'t do that", code: "Forbidden"}); diff --git a/client/helpers/memory.js b/client/helpers/memory.js index e70bcd43..da2c2ac2 100644 --- a/client/helpers/memory.js +++ b/client/helpers/memory.js @@ -3,11 +3,10 @@ function Memory(){ return { get: function(key){ - if(!data[key]) return null; + if(data[key] === undefined) return null; return data[key]; }, set: function(key, value){ - if(!data[key]) data[key] = {}; data[key] = value; }, all: function(){ diff --git a/client/index.js b/client/index.js index 0d71aac1..2166b1f1 100644 --- a/client/index.js +++ b/client/index.js @@ -4,19 +4,15 @@ import Router from './router'; import './assets/css/reset.scss'; -if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('/assets/worker/cache.js').catch(function(error) { - console.log('ServiceWorker registration failed:', error); - }); -} - -window.onload = () => { - ReactDOM.render(, document.getElementById('main')); -}; - -window.log = function(){console.log.apply(this, arguments)}; - window.addEventListener("DOMContentLoaded", () => { const className = 'ontouchstart' in window ? 'touch-yes' : 'touch-no'; document.body.classList.add(className); + + ReactDOM.render(, document.getElementById('main')); + + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/assets/worker/cache.js').catch(function(error) { + console.log('ServiceWorker registration failed:', error); + }); + } }); diff --git a/client/model/session.js b/client/model/session.js index d5222fa2..10e4d086 100644 --- a/client/model/session.js +++ b/client/model/session.js @@ -1,7 +1,7 @@ import { http_get, http_post, http_delete } from '../helpers/'; class SessionManager{ - isLoggedIn(){ + currentUser(){ let url = '/api/session' return http_get(url) .then(data => data.result); diff --git a/client/pages/connectpage.js b/client/pages/connectpage.js index 2ca36247..554fb6ae 100644 --- a/client/pages/connectpage.js +++ b/client/pages/connectpage.js @@ -45,8 +45,10 @@ export class ConnectPage extends React.Component { authenticate(params){ this.setState({loading: true}); Session.authenticate(params) - .then((path) => { + .then(Session.currentUser) + .then((user) => { let url = '/files/'; + let path = user.home if(path){ path = path.replace(/^\/?(.*?)\/?$/, "$1"); if(path !== ""){ diff --git a/client/pages/error.scss b/client/pages/error.scss index 5448fdfd..25a2bd72 100644 --- a/client/pages/error.scss +++ b/client/pages/error.scss @@ -2,6 +2,7 @@ width: 80%; max-width: 600px; margin: 50px auto 0 auto; + flex-direction: column; h1{margin: 5px 0; font-size: 3.1em;} h2{margin: 10px 0; font-weight: normal;} diff --git a/client/pages/filespage.js b/client/pages/filespage.js index c511b7d0..ea3244ba 100644 --- a/client/pages/filespage.js +++ b/client/pages/filespage.js @@ -6,7 +6,7 @@ import './filespage.scss'; import './error.scss'; import { Files } from '../model/'; import { sort, onCreate, onRename, onDelete, onUpload, onSearch } from './filespage.helper'; -import { NgIf, Loader, EventReceiver } from '../components/'; +import { NgIf, Loader, EventReceiver, LoggedInOnly, ErrorPage } from '../components/'; import { notify, debounce, goToFiles, goToViewer, event, settings_get, settings_put } from '../helpers/'; import { BreadCrumb, FileSystem, FrequentlyAccess, Submenu } from './filespage/'; import InfiniteScroll from 'react-infinite-scroller'; @@ -14,6 +14,8 @@ import InfiniteScroll from 'react-infinite-scroller'; const PAGE_NUMBER_INIT = 3; const LOAD_PER_SCROLL = 24; +@ErrorPage +@LoggedInOnly @EventReceiver @DragDropContext(('ontouchstart' in window)? HTML5Backend : HTML5Backend) export class FilesPage extends React.Component { @@ -30,8 +32,7 @@ export class FilesPage extends React.Component { metadata: null, frequents: [], page_number: PAGE_NUMBER_INIT, - loading: true, - error: null + loading: true }; this.goToFiles = goToFiles.bind(null, this.props.history); @@ -50,7 +51,6 @@ export class FilesPage extends React.Component { this.props.subscribe('file.delete', onDelete.bind(this)); this.props.subscribe('file.refresh', this.onRefresh.bind(this)); window.addEventListener('keydown', this.toggleHiddenFilesVisibilityonCtrlK); - this.hideError(); } componentWillUnmount() { @@ -76,10 +76,6 @@ export class FilesPage extends React.Component { } } - hideError(){ - this.setState({error: null}); - } - toggleHiddenFilesVisibilityonCtrlK(e){ if(e.keyCode === 72 && e.ctrlKey === true){ e.preventDefault(); @@ -118,10 +114,9 @@ export class FilesPage extends React.Component { notify.send(res, 'error'); } }, (error) => { - this.setState({error: error}); + this.props.error(error); }); this.observers.push(observer); - this.setState({error: null}); if(path === "/"){ Files.frequents().then((s) => this.setState({frequents: s})); } @@ -213,7 +208,7 @@ export class FilesPage extends React.Component {
70} initialLoad={false} useWindow={false} loadMore={this.loadMore.bind(this)} threshold={100}> - + @@ -225,14 +220,9 @@ export class FilesPage extends React.Component { - + - -

Oops!

-

It seems this directory doesn't exist

-

{JSON.stringify(this.state.error)}

-
diff --git a/client/pages/homepage.js b/client/pages/homepage.js index 226497d8..810917fe 100644 --- a/client/pages/homepage.js +++ b/client/pages/homepage.js @@ -13,10 +13,14 @@ export class HomePage extends React.Component { } componentDidMount(){ - Session.isLoggedIn() + Session.currentUser() .then((res) => { - if(res === true){ - this.setState({redirection: "/files"}); + if(res && res.is_authenticated === true){ + let url = "/files" + if(res.home){ + url += res.home + } + this.setState({redirection: url}); }else{ this.setState({redirection: "/login"}); } diff --git a/client/pages/viewerpage.js b/client/pages/viewerpage.js index ce9adcab..5284a99e 100644 --- a/client/pages/viewerpage.js +++ b/client/pages/viewerpage.js @@ -4,7 +4,7 @@ import Path from 'path'; import './viewerpage.scss'; import './error.scss'; import { Files } from '../model/'; -import { BreadCrumb, Bundle, NgIf, Loader, Container, EventReceiver, EventEmitter } from '../components/'; +import { BreadCrumb, Bundle, NgIf, Loader, Container, EventReceiver, EventEmitter, LoggedInOnly , ErrorPage } from '../components/'; import { debounce, opener, notify } from '../helpers/'; import { AudioPlayer, FileDownloader, ImageViewer, PDFViewer } from './viewerpage/'; @@ -19,6 +19,8 @@ const IDE = (props) => ( ); +@ErrorPage +@LoggedInOnly @EventReceiver export class ViewerPage extends React.Component { constructor(props){ @@ -31,8 +33,7 @@ export class ViewerPage extends React.Component { content: null, needSaving: false, isSaving: false, - loading: true, - error: null + loading: true }; this.props.subscribe('file.select', this.onPathUpdate.bind(this)); } @@ -41,10 +42,10 @@ export class ViewerPage extends React.Component { this.setState({ path: props.match.url.replace('/view', '') + (location.hash || ""), filename: Path.basename(props.match.url.replace('/view', '')) || 'untitled.dat' - }, () => { this.componentWillMount(); }); + }, () => { this.componentDidMount(); }); } - componentWillMount(){ + componentDidMount(){ const metadata = () => { return new Promise((done, err) => { let app_opener = opener(this.state.path); @@ -54,7 +55,7 @@ export class ViewerPage extends React.Component { opener: app_opener }, () => done(app_opener)); }).catch(error => { - notify.send(err, 'error'); + this.props.error(error); err(error); }); }); @@ -67,15 +68,14 @@ export class ViewerPage extends React.Component { if(err && err.code === 'BINARY_FILE'){ this.setState({opener: 'download', loading: false}); }else{ - this.setState({error: err}); + this.props.error(err); } }); - }else{ - this.setState({loading: false}); + return; } + this.setState({loading: false}); }; - return metadata() - .then(data_fetch); + return metadata().then(data_fetch); } componentWillUnmount() { @@ -106,7 +106,7 @@ export class ViewerPage extends React.Component { if(err && err.code === 'CANCELLED'){ return; } this.setState({isSaving: false}); notify.send(err, 'error'); - return Promise.reject(); + return Promise.reject(err); }); } @@ -125,7 +125,7 @@ export class ViewerPage extends React.Component {
- + - + - -

Oops!

-

There is nothing in here

-

{JSON.stringify(this.state.error)}

-
); diff --git a/server/common/utils.go b/server/common/utils.go index e0d37a7a..56c56ca7 100644 --- a/server/common/utils.go +++ b/server/common/utils.go @@ -19,6 +19,9 @@ func NewBool(t bool) *bool { } func NewString(t string) *string { + if t == "" { + return nil + } return &t } diff --git a/server/ctrl/files.go b/server/ctrl/files.go index 22b71c8d..1d2efdbc 100644 --- a/server/ctrl/files.go +++ b/server/ctrl/files.go @@ -56,6 +56,13 @@ func FileLs(ctx App, res http.ResponseWriter, req *http.Request) { } func FileCat(ctx App, res http.ResponseWriter, req *http.Request) { + http.SetCookie(res, &http.Cookie{ + Name: "download", + Value: "", + MaxAge: -1, + Path: "/", + }) + path, err := pathBuilder(ctx, req.URL.Query().Get("path")) if err != nil { SendErrorResult(res, err) @@ -71,13 +78,6 @@ func FileCat(ctx App, res http.ResponseWriter, req *http.Request) { return } - http.SetCookie(res, &http.Cookie{ - Name: "download", - Value: "", - MaxAge: -1, - Path: "/", - }) - file, err = services.ProcessFileBeforeSend(file, &ctx, req, &res) if err != nil { SendErrorResult(res, err) diff --git a/server/ctrl/session.go b/server/ctrl/session.go index 800f593a..e1591e05 100644 --- a/server/ctrl/session.go +++ b/server/ctrl/session.go @@ -8,21 +8,28 @@ import ( "time" ) -func SessionIsValid(ctx App, res http.ResponseWriter, req *http.Request) { +type Session struct { + Home *string `json:"home,omitempty"` + IsAuth bool `json:"is_authenticated"` +} + +func SessionGet(ctx App, res http.ResponseWriter, req *http.Request) { + r := Session { + IsAuth: false, + } + if ctx.Backend == nil { - SendSuccessResult(res, false) + SendSuccessResult(res, r) return } - if _, err := ctx.Backend.Ls("/"); err != nil { - SendSuccessResult(res, false) + home, err := model.GetHome(ctx.Backend) + if err != nil { + SendSuccessResult(res, r) return } - home, _ := model.GetHome(ctx.Backend) - if home == "" { - SendSuccessResult(res, true) - return - } - SendSuccessResult(res, true) + r.IsAuth = true + r.Home = NewString(home) + SendSuccessResult(res, r) } func SessionAuthenticate(ctx App, res http.ResponseWriter, req *http.Request) { diff --git a/server/ctrl/share.go b/server/ctrl/share.go index 1488ad00..91fbe7a6 100644 --- a/server/ctrl/share.go +++ b/server/ctrl/share.go @@ -5,7 +5,6 @@ import ( . "github.com/mickael-kerjean/nuage/server/common" "github.com/mickael-kerjean/nuage/server/model" "net/http" - "log" ) func ShareList(ctx App, res http.ResponseWriter, req *http.Request) { @@ -29,7 +28,6 @@ func ShareGet(ctx App, res http.ResponseWriter, req *http.Request) { func ShareUpsert(ctx App, res http.ResponseWriter, req *http.Request) { s := extractParams(req, &ctx) - log.Println("EXPIRE::", s.Expire, ctx.Body["expire"]) s.Path = NewStringFromInterface(ctx.Body["path"]) if err := model.ShareUpsert(&s); err != nil { diff --git a/server/router/index.go b/server/router/index.go index d826945d..898ccbf1 100644 --- a/server/router/index.go +++ b/server/router/index.go @@ -14,7 +14,7 @@ func Init(a *App) *http.Server { // API session := r.PathPrefix("/api/session").Subrouter() - session.HandleFunc("", APIHandler(SessionIsValid, *a)).Methods("GET") + session.HandleFunc("", APIHandler(SessionGet, *a)).Methods("GET") session.HandleFunc("", APIHandler(SessionAuthenticate, *a)).Methods("POST") session.HandleFunc("", APIHandler(SessionLogout, *a)).Methods("DELETE") session.Handle("/auth/{service}", APIHandler(SessionOAuthBackend, *a)).Methods("GET") From 434f1a90c35418b6d37feb42c819f080fc5de85c Mon Sep 17 00:00:00 2001 From: Mickael KERJEAN Date: Wed, 26 Sep 2018 03:23:01 +1000 Subject: [PATCH 3/5] feature (scroll memory): make it easier to scroll/select on long list of things --- client/pages/filespage.js | 28 ++++++++++++++++++++---- client/pages/filespage/thing-existing.js | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/client/pages/filespage.js b/client/pages/filespage.js index ea3244ba..8178595d 100644 --- a/client/pages/filespage.js +++ b/client/pages/filespage.js @@ -11,8 +11,15 @@ import { notify, debounce, goToFiles, goToViewer, event, settings_get, settings_ import { BreadCrumb, FileSystem, FrequentlyAccess, Submenu } from './filespage/'; import InfiniteScroll from 'react-infinite-scroller'; -const PAGE_NUMBER_INIT = 3; -const LOAD_PER_SCROLL = 24; +const PAGE_NUMBER_INIT = 2; +const LOAD_PER_SCROLL = 48; + +// usefull when user press the back button while keeping the current context +let LAST_PAGE_PARAMS = { + path: null, + scroll: 0, + page_number: PAGE_NUMBER_INIT +}; @ErrorPage @LoggedInOnly @@ -61,6 +68,10 @@ export class FilesPage extends React.Component { this.props.unsubscribe('file.refresh'); window.removeEventListener('keydown', this.toggleHiddenFilesVisibilityonCtrlK); this._cleanupListeners(); + + LAST_PAGE_PARAMS.path = this.state.path; + LAST_PAGE_PARAMS.scroll = this.refs.$scroll.scrollTop; + LAST_PAGE_PARAMS.page_number = this.state.page_number; } componentWillReceiveProps(nextProps){ @@ -108,7 +119,16 @@ export class FilesPage extends React.Component { metadata: res.metadata, files: sort(files, this.state.sort), loading: false, - page_number: PAGE_NUMBER_INIT + page_number: function(){ + if(this.state.path === LAST_PAGE_PARAMS.path){ + return LAST_PAGE_PARAMS.page_number; + } + return PAGE_NUMBER_INIT; + }.bind(this)() + }, () => { + if(this.state.path === LAST_PAGE_PARAMS.path){ + this.refs.$scroll.scrollTop = LAST_PAGE_PARAMS.scroll; + } }); }else{ notify.send(res, 'error'); @@ -205,7 +225,7 @@ export class FilesPage extends React.Component {
-
+
70} initialLoad={false} useWindow={false} loadMore={this.loadMore.bind(this)} threshold={100}> diff --git a/client/pages/filespage/thing-existing.js b/client/pages/filespage/thing-existing.js index 71935089..fc16849c 100644 --- a/client/pages/filespage/thing-existing.js +++ b/client/pages/filespage/thing-existing.js @@ -408,7 +408,7 @@ class LazyLoadImage extends React.Component { error: false }; this.$scroll = document.querySelector(props.scroller); - this.onScroll = debounce(this.onScroll.bind(this), 100); + this.onScroll = debounce(this.onScroll.bind(this), 250); } componentDidMount(){ From 3d938d8632853ed91a2ff9a66f28c0141aea4f01 Mon Sep 17 00:00:00 2001 From: Mickael KERJEAN Date: Wed, 26 Sep 2018 10:51:29 +1000 Subject: [PATCH 4/5] build (ci): fix build issue --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 5362c2e4..8c9386e4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -10,5 +10,5 @@ pipeline: commands: - echo $DOCKER_PASSWORD | docker login -u=$DOCKER_USERNAME --password-stdin - docker pull alpine:latest - - docker build --no-cache -t machines/nuage:master docker/prod + - docker build --no-cache -t machines/nuage docker/prod - docker push machines/nuage \ No newline at end of file From 0de43c72b000745a21df7282f6b8df486f6c8de4 Mon Sep 17 00:00:00 2001 From: Lewis Cowles Date: Thu, 27 Sep 2018 13:38:06 +0100 Subject: [PATCH 5/5] improvement (font): Embedded Google Font (#106) Fixes #102 --- client/assets/css/reset.scss | 189 ++++++++++++------ .../SourceCodePro-Regular-400-latin-ext.woff2 | Bin 0 -> 11008 bytes .../SourceCodePro-Regular-400-latin.woff2 | Bin 0 -> 13172 bytes ...SourceCodePro-Semibold-600-latin-ext.woff2 | Bin 0 -> 10928 bytes .../SourceCodePro-Semibold-600-latin.woff2 | Bin 0 -> 12956 bytes webpack.config.js | 18 +- 6 files changed, 136 insertions(+), 71 deletions(-) create mode 100644 client/assets/fonts/SourceCodePro-Regular-400-latin-ext.woff2 create mode 100644 client/assets/fonts/SourceCodePro-Regular-400-latin.woff2 create mode 100644 client/assets/fonts/SourceCodePro-Semibold-600-latin-ext.woff2 create mode 100644 client/assets/fonts/SourceCodePro-Semibold-600-latin.woff2 diff --git a/client/assets/css/reset.scss b/client/assets/css/reset.scss index 795f8cd4..16165a94 100644 --- a/client/assets/css/reset.scss +++ b/client/assets/css/reset.scss @@ -1,56 +1,118 @@ -@import url('https://fonts.googleapis.com/css?family=Source+Code+Pro:400,600'); +/* latin-ext */ + +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Code Pro'), local('SourceCodePro-Regular'), url(/assets/fonts/SourceCodePro-Regular-400-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + + +/* latin */ + +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Code Pro'), local('SourceCodePro-Regular'), url(/assets/fonts/SourceCodePro-Regular-400-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + + +/* latin-ext */ + +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 600; + src: local('Source Code Pro Semibold'), local('SourceCodePro-Semibold'), url(/assets/fonts/SourceCodePro-Semibold-600-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + + +/* latin */ + +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 600; + src: local('Source Code Pro Semibold'), local('SourceCodePro-Semibold'), url(/assets/fonts/SourceCodePro-Semibold-600-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} :root { --bg-color: #f1f2f2; --color: #626469; --emphasis: #375160; - --primary: #9AD1ED; --emphasis-primary: #c5e2f1; - --secondary: #466372; --emphasis-secondary: #466372; - --light: #909090; - --super-light: #f4f4f4; + --super-light: #f4f4f4; --error: #f26d6d; --success: #63d9b1; - --dark: #313538; } html { - font-family:"San Francisco","Roboto","Arial",sans-serif; - -webkit-text-size-adjust:100%; + font-family: "San Francisco", "Roboto", "Arial", sans-serif; + -webkit-text-size-adjust: 100%; background: var(--bg-color); color: var(--color); } -body {overflow: hidden;} -body, html{ + +body { + overflow: hidden; +} + +body, +html { height: 100%; margin: 0; } -#main{height: 100%;} -a{color: inherit; text-decoration: none;} -select{-moz-appearance: none;} +#main { + height: 100%; +} + +a { + color: inherit; + text-decoration: none; +} + +select { + -moz-appearance: none; +} + select:-moz-focusring { color: transparent; outline: none; border: none; } -select::-ms-expand { display: none; } - -button::-moz-focus-inner { - border: 0; +select::-ms-expand { + display: none; } -input, textarea{ +button::-moz-focus-inner { + border: 0; +} + +input, +textarea { transition: border 0.2s; outline: none; } -input[type="checkbox"]{position: relative; top: 1px; margin: 0; padding: 0; vertical-align: top;} + +input[type="checkbox"] { + position: relative; + top: 1px; + margin: 0; + padding: 0; + vertical-align: top; +} .no-select { -webkit-touch-callout: none; @@ -61,74 +123,73 @@ input[type="checkbox"]{position: relative; top: 1px; margin: 0; padding: 0; vert user-select: none; } - - button:focus, -a:focus, a:active, +a:focus, +a:active, button::-moz-focus-inner, input[type="reset"]::-moz-focus-inner, input[type="button"]::-moz-focus-inner, input[type="submit"]::-moz-focus-inner, select::-moz-focus-inner, -input[type="file"] > input[type="button"]::-moz-focus-inner { - outline: none !important; +input[type="file"]>input[type="button"]::-moz-focus-inner { + outline: none !important; } select:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 #000; + color: transparent; + text-shadow: 0 0 0 #000; } - - - - - - -.connect-form input:hover, .connect-form textarea:hover, -.connect-form input:focus, .connect-form textarea:focus{ +.connect-form input:hover, +.connect-form textarea:hover, +.connect-form input:focus, +.connect-form textarea:focus { border-color: rgb(154, 209, 237)!important; } -.drag-drop{ +.drag-drop { z-index: 2; } -.drag-drop.dragging > div{ - background: rgba(0,0,0,0.1); + +.drag-drop.dragging>div { + background: rgba(0, 0, 0, 0.1); } - - /* CONNECTION FORM */ -.login-form button.active{ - box-shadow: 0px 1px 5px rgba(0,0,0,0.20); + +.login-form button.active { + box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.20); } +::-webkit-scrollbar { + height: 4px; + width: 4px +} +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, .1) +} -::-webkit-scrollbar{ - height:4px; - width:4px +::-webkit-scrollbar-thumb { + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + -ms-border-radius: 2px; + -o-border-radius: 2px; + border-radius: 2px; + background: rgba(0, 0, 0, .2); } -::-webkit-scrollbar-track{ - background:rgba(0,0,0,.1) + +.scroll-y { + scrollbar-3dlight-color: #7d7e94; + scrollbar-arrow-color: #c1c1d1; + scrollbar-darkshadow-color: #2d2c4d; + scrollbar-face-color: rgba(0, 0, 0, .1); + scrollbar-highlight-color: #7d7e94; + scrollbar-shadow-color: #2d2c4d; + scrollbar-track-color: rgba(0, 0, 0, .1); } -::-webkit-scrollbar-thumb{ - -webkit-border-radius:2px; - -moz-border-radius:2px; - -ms-border-radius:2px; - -o-border-radius:2px; - border-radius:2px; - background:rgba(0,0,0,.2); -} -.scroll-y{ - scrollbar-3dlight-color:#7d7e94; - scrollbar-arrow-color:#c1c1d1; - scrollbar-darkshadow-color:#2d2c4d; - scrollbar-face-color:rgba(0,0,0,.1); - scrollbar-highlight-color:#7d7e94; - scrollbar-shadow-color:#2d2c4d; - scrollbar-track-color:rgba(0,0,0,.1); -} -.pointer{cursor: pointer;} + +.pointer { + cursor: pointer; +} \ No newline at end of file diff --git a/client/assets/fonts/SourceCodePro-Regular-400-latin-ext.woff2 b/client/assets/fonts/SourceCodePro-Regular-400-latin-ext.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..efa1cebdfa8962248a976f1a5fb7579d202f0511 GIT binary patch literal 11008 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 literal 0 HcmV?d00001 diff --git a/client/assets/fonts/SourceCodePro-Regular-400-latin.woff2 b/client/assets/fonts/SourceCodePro-Regular-400-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..68170193153df9f171da35e9de8809b4c28593b9 GIT binary patch literal 13172 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 S`p-wJ#r+3;H~gm29{>Ov@B;t< literal 0 HcmV?d00001 diff --git a/client/assets/fonts/SourceCodePro-Semibold-600-latin.woff2 b/client/assets/fonts/SourceCodePro-Semibold-600-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1c6ca4c94f800907ca74feeea5f414099b187dda GIT binary patch literal 12956 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%0D~HQwp+eBl-rkxnPF z*cp7}44_I^_nW}I`s&U8d#PkPf(GSMC(P2R=w$*m#|l>rGD%ZHTaX2sX{Nb3ZNK>^ z&TLdJP(2Xx)%NMW^zudLw0kHmmW`rafGG9RBjV#=89Pq z%B&SLNx|lzSWWn8ICa(j8h`Y~!i9HI#SWDh1_#EE2f>4fpnp%uP1gL`gWkSK&|ZFg zSxZ2&Lys?M38e%r@@wihJS7L6dUB|RW8t(6ok$ZT`J}AC6LX5ttmRXWOYli zB{qm{NsbWW<{|s09;5QLW}!LdNr2lQq8wY&hvY%r!knogNN8#fAL8dA7A!eN`GZX( zvZP5qLD~uD@h4N!93>Cye5PG=hUrV zW8!QUCLPt?$V=f&$ig&rEbBh&f`f%PbB_`8!|Ev*oUF%yemZ~SmlM;*6RGbJ$OR8A zga-=%{^ObF?x0E0tNLuVBEpO_s#bhUqq-sD@vU?HgDV+$xF(asH%fB>BLn#q8jOUf zw80B#n~RgnGUH5m8;^&#o6I;C%Yu^<>^vU9Cbi(k6N?%`RBCZ!5s_qRET&RJ4Hj;U zkNlCE2_$d}WJDcRFS6ET3DQzQ8CtEOR_kU!a`g-r_61BtD46LQmzGXS#D-`rCq36i z?e@E=T`G@<1pv1%=Faiqr0OP-FCtu4UCm$Yu4v-t$=o!F4i%Do-GvtrQTOXwic+jR z?Y9?P`9hdiz$Rw1}`DFpK<7JaQ+7dW84w zPYk7ACWd|8WE8bo{^H#X4ZlJm2H=P&PhjPww;?kPgY!2i>kaUL4VhE z$gxh01pfZVlXVlE%RxAO3?v4x2RWCw?-4Ci?eBV4?_bfrVxM}S>T|zzx2QSKGqFOT z5f>4RDnV$4PBASZJuxvYUa9!HQdn%!l#p^HC2X)RZ)Ou!o9|AwW@=DzDG5oL312jC z)6LF&74zKQ7WtLO!hE?x=zKKDxNadxs*TlU(6K$uQevRR$+%K$j7<4lmt*1krAv+*?IYrKTpOjrBq%;1^YRP(!b zvU)3@a`<<76dx0btpbJV*%@h&gsRlSMmq1ue()lTkc0a-;b&wP6&G#abiccJO%XJy zU)t+$FN4a}MW%l@)PAnadi6s8x*v(GDNzVPs-+)jThi)7Z#+Z$!(fqEyLmgB-?yQqT}vZ2g!$|UNYd#c)W$ENNNWQ z-w;EVY{Y(d5d*?pVgzMM8S8-q_Wr?BFrD9c0=(&uVDpg3`7qdgLwKpVC-_si zoT+?Z35UD*R@OI2#ELA~Yy@%^GA(w_nzKbjExx+u|Au15<=^qpa?3qWFVT@3zp;!U zK(%e4l)K!=;U3|_@v~~Y5)a$`+{J!HWQu>LFeOubb|w{7XO^XvlS4)<_Sp~Z;* z!5!>XknFNkJ;Hl<(?KjnmUjCUSl8x=<- zT6$NtHhR8Y#{K^?yd%@g=8lH}0U>WzvwU) zE9%oQC)&43gY49VG1L-pm3f#A&v2}6E+)sFp#ov#SW;ypBa9(dus}H(>$geG4~QN{ zumA2Rn&C(EyF568rU85YAa)RUIYRVZ|Jj5r;8^iQ=gxbrbyS8~j-^3=0EaQe%Ds_I zWVmBc4gMwzm(`hJTZ6ruVvSGT8%?x}$p>*6(r(&1I7{-vb;Z(Ff5j*?-e45W0U<3p z4(wfHCl

Hph+44CM()VZXKash;i5Pc4 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 S7Yh>WnDsHlixb$03jhG>&)*3E literal 0 HcmV?d00001 diff --git a/webpack.config.js b/webpack.config.js index 4e646a52..dc4bb9d5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,8 +19,7 @@ let config = { chunkFilename: "assets/js/chunk_[name]_[id]_[chunkhash].js" }, module: { - rules: [ - { + rules: [{ test: path.join(__dirname, 'client'), use: ['babel-loader'], exclude: /node_modules/ @@ -29,6 +28,10 @@ let config = { test: /\.html$/, loader: 'html-loader' }, + { + test: /\.woff2$/, + loader: 'woff-loader' + }, { test: /\.scss$/, loaders: ['style-loader', 'css-loader', 'sass-loader'] @@ -55,20 +58,21 @@ let config = { new webpack.optimize.OccurrenceOrderPlugin(), new HtmlWebpackPlugin({ template: path.join(__dirname, 'client', 'index.html'), - inject:true + inject: true }), new CopyWebpackPlugin([ { from: 'manifest.json', to: "assets/" }, { from: 'worker/*.js', to: "assets/" }, { from: 'assets/logo/*' }, - { from: 'assets/icons/*' } + { from: 'assets/icons/*' }, + { from: 'assets/fonts/*' } ], { context: path.join(__dirname, 'client') }), //new BundleAnalyzerPlugin() ] }; -if(process.env.NODE_ENV === 'production'){ +if (process.env.NODE_ENV === 'production') { config.plugins.push(new UglifyJSPlugin({ sourceMap: false })); @@ -79,8 +83,8 @@ if(process.env.NODE_ENV === 'production'){ threshold: 0, minRatio: 0.8 })); -}else{ +} else { config.devtool = '#inline-source-map'; } -module.exports = config; +module.exports = config; \ No newline at end of file