diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..c13c5f62 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..43eb113c --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +# Created by https://www.gitignore.io/api/osx,windows,node + +build/ + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +### +.env + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/osx,windows,node diff --git a/README.md b/README.md deleted file mode 100644 index f0bc82af..00000000 --- a/README.md +++ /dev/null @@ -1,72 +0,0 @@ -![cf](https://i.imgur.com/7v5ASc8.png) Lab 25 - Client Side Auth -====== - -## To Submit this Assignment - * create a fork of this repository - * push to your repository - * submit a pull request to this repository - * submit a link to your PR in canvas - * write a question and observation on canvas - -## Include - * `.eslintrc` - * `.babelrc` - * `.gitignore` - * `package.json` - * create an npm `build` script for running `webpack` - * create an npm `build-watch` script for running `webpack-dev-server --inline --hot` - * create an npm `test` script for running karma and all associated tests - * create an npm `test-watch` script for running karma on file system changes - * create an npm `lint` script for linting your JS with `eslint` - * **ignore the build directory** - * `webpack.config.js` - * this should include all of the production environment configurations used in lecture code - * `karma.config.js` - -## Description - * Create these directories to organize your code: - * app - * app/config - * app/view - * app/view/home - * app/view/landing - * app/scss - * app/scss/lib - * app/scss/lib/base - * app/scss/lib/layout - * app/scss/lib/theme - * app/service - * app/component - * app/component/landing - * include a **main.scss** - * include an `.scss` partial for each component you create - * style the application to meet the **sign in** & **sign up** mockups provided in the `wireframes` directory of this repo - * use `require.context` to add all of your angular construct definitions - -## Clone - * Clone, setup, and run the `slugram-backend` application in order for your angular app to communicate with the server - * [slugram-backend](https://github.com/slugbyte/slugram-backend) - * **note** - to access the deployed backend use the following: - * `https://slugram-backend.herokuapp.com` - * **note** - if you are using the cloned version, be sure to add your `.env` file - you can use the same one we used for our lecture 18 deployment - * **note** - this application should be running on the `staging` branch - * **note** - **DO NOT** include this application with your assignment submission - -## Functional Requirements - * Create 2 views: `/#/`, `/#/home/` - * each view should have its own controller - * the `/#/` view should be the default landing page - * the landing page markup should contain `` and `` components - * Create an auth service with for making `http` requests to the `slugram-backend` application - * this should have contain methods for sign up and sign in routes - * this should have methods to `get` and `delete` a token from local storage - * Create a sign up component - * this should have its own controller and use the `controllerAs` syntax - * this should have a form with username, email, and password fields - * this should use the auth service to signup and store a token - * this should redirect the page to `/#/home` on a successful form submit - * Create a login component - * this should have its own controller and use the `controllerAs` syntax - * this should have a form with username and password fields - * this should use the auth service to login and store a token - * this should redirect the page to `/#/home` on success diff --git a/app/component/gallery/create-gallery/_create-gallery.scss b/app/component/gallery/create-gallery/_create-gallery.scss new file mode 100644 index 00000000..ba7410c5 --- /dev/null +++ b/app/component/gallery/create-gallery/_create-gallery.scss @@ -0,0 +1 @@ +@import "../../../scss/lib/theme/vars"; diff --git a/app/component/gallery/create-gallery/create-gallery.html b/app/component/gallery/create-gallery/create-gallery.html new file mode 100644 index 00000000..c1adb902 --- /dev/null +++ b/app/component/gallery/create-gallery/create-gallery.html @@ -0,0 +1,27 @@ + diff --git a/app/component/gallery/create-gallery/create-gallery.js b/app/component/gallery/create-gallery/create-gallery.js new file mode 100644 index 00000000..3976c301 --- /dev/null +++ b/app/component/gallery/create-gallery/create-gallery.js @@ -0,0 +1,24 @@ +'use strict'; + +require('./_create-gallery.scss'); + +module.exports = { + template: require('./create-gallery.html'), + controller: ['$log', 'galleryService', CreateGalleryController], + controllerAs: 'createGalleryCtrl' +}; + +function CreateGalleryController($log, galleryService) { + $log.debug('CreateGalleryController'); + + this.gallery = {}; + + this.createGallery = function() { + galleryService.createGallery(this.gallery) + .then( () => { + this.gallery.name = null; + this.gallery.desc = null; + }); + }; +}; + diff --git a/app/component/gallery/edit-gallery/_edit-gallery.scss b/app/component/gallery/edit-gallery/_edit-gallery.scss new file mode 100644 index 00000000..ba7410c5 --- /dev/null +++ b/app/component/gallery/edit-gallery/_edit-gallery.scss @@ -0,0 +1 @@ +@import "../../../scss/lib/theme/vars"; diff --git a/app/component/gallery/edit-gallery/edit-gallery.html b/app/component/gallery/edit-gallery/edit-gallery.html new file mode 100644 index 00000000..f67339a1 --- /dev/null +++ b/app/component/gallery/edit-gallery/edit-gallery.html @@ -0,0 +1,19 @@ +
+ +
diff --git a/app/component/gallery/edit-gallery/edit-gallery.js b/app/component/gallery/edit-gallery/edit-gallery.js new file mode 100644 index 00000000..8a138909 --- /dev/null +++ b/app/component/gallery/edit-gallery/edit-gallery.js @@ -0,0 +1,20 @@ +'use strict'; + +require('./_edit-gallery.scss'); + +module.exports = { + template: require('./edit-gallery.html'), + controller: ['$log', 'galleryService', EditGalleryController], + controllerAs: 'editGalleryCtrl', + bindings: { + gallery: '<' + } +}; + +function EditGalleryController($log, galleryService) { + $log.debug('EditGalleryController'); + + this.updateGallery = function() { + galleryService.updateGallery(this.gallery._id, this.gallery); + }; +}; diff --git a/app/component/gallery/gallery-item/_gallery-item.scss b/app/component/gallery/gallery-item/_gallery-item.scss new file mode 100644 index 00000000..ba7410c5 --- /dev/null +++ b/app/component/gallery/gallery-item/_gallery-item.scss @@ -0,0 +1 @@ +@import "../../../scss/lib/theme/vars"; diff --git a/app/component/gallery/gallery-item/gallery-item.html b/app/component/gallery/gallery-item/gallery-item.html new file mode 100644 index 00000000..3be8f9c7 --- /dev/null +++ b/app/component/gallery/gallery-item/gallery-item.html @@ -0,0 +1,19 @@ + +
  • + edit + delete +
  • diff --git a/app/component/gallery/gallery-item/gallery-item.js b/app/component/gallery/gallery-item/gallery-item.js new file mode 100644 index 00000000..18453222 --- /dev/null +++ b/app/component/gallery/gallery-item/gallery-item.js @@ -0,0 +1,22 @@ +'use strict'; + +require('./_gallery-item.scss'); + +module.exports = { + template: require('./gallery-item.html'), + controller: ['$log', 'galleryService', GalleryItemController], + controllerAs: 'galleryItemCtrl', + bindings: { + gallery: '<' + } +}; + +function GalleryItemController($log, galleryService) { + $log.debug('GalleryItemController'); + + this.showEditGallery = false; + + this.deleteGallery = function() { + galleryService.deleteGallery(this.gallery._id); + }; +}; diff --git a/app/component/gallery/thumbnail-container/_thumbnail-container.scss b/app/component/gallery/thumbnail-container/_thumbnail-container.scss new file mode 100644 index 00000000..ba7410c5 --- /dev/null +++ b/app/component/gallery/thumbnail-container/_thumbnail-container.scss @@ -0,0 +1 @@ +@import "../../../scss/lib/theme/vars"; diff --git a/app/component/gallery/thumbnail-container/thumbnail-container.html b/app/component/gallery/thumbnail-container/thumbnail-container.html new file mode 100644 index 00000000..991ec8ba --- /dev/null +++ b/app/component/gallery/thumbnail-container/thumbnail-container.html @@ -0,0 +1,8 @@ +
    +

    {{ thumbnailContainerCtrl.gallery.name }}

    + + +
    + +
    +
    diff --git a/app/component/gallery/thumbnail-container/thumbnail-container.js b/app/component/gallery/thumbnail-container/thumbnail-container.js new file mode 100644 index 00000000..18bf16d4 --- /dev/null +++ b/app/component/gallery/thumbnail-container/thumbnail-container.js @@ -0,0 +1,11 @@ +'use strict'; + +require('./_thumbnail-container.scss'); + +module.exports = { + template: require('./thumbnail-container.html'), + controllerAs: 'thumbnailContainerCtrl', + bindings: { + gallery: '<' + } +}; diff --git a/app/component/gallery/thumbnail/_thumbnail.scss b/app/component/gallery/thumbnail/_thumbnail.scss new file mode 100644 index 00000000..ba7410c5 --- /dev/null +++ b/app/component/gallery/thumbnail/_thumbnail.scss @@ -0,0 +1 @@ +@import "../../../scss/lib/theme/vars"; diff --git a/app/component/gallery/thumbnail/thumbnail.html b/app/component/gallery/thumbnail/thumbnail.html new file mode 100644 index 00000000..27a44836 --- /dev/null +++ b/app/component/gallery/thumbnail/thumbnail.html @@ -0,0 +1,4 @@ +
    + {{ thumbnailCtrl.pic.desc }} + delete +
    diff --git a/app/component/gallery/thumbnail/thumbnail.js b/app/component/gallery/thumbnail/thumbnail.js new file mode 100644 index 00000000..fe96846a --- /dev/null +++ b/app/component/gallery/thumbnail/thumbnail.js @@ -0,0 +1,20 @@ +'use strict'; + +require('./_thumbnail.scss'); + +module.exports = { + template: require('./thumbnail.html'), + controller: ['$log', 'picService', ThumbnailController], + controllerAs: 'thumbnailCtrl', + bindings: { + pic: '<' + } +}; + +function ThumbnailController($log, picService) { + $log.debug('ThumbnailController'); + + this.deletePic = function() { + $log.debug('thumbnailCtrl.deletePic'); + }; +}; diff --git a/app/component/gallery/upload-pic/_upload-pic.scss b/app/component/gallery/upload-pic/_upload-pic.scss new file mode 100644 index 00000000..ba7410c5 --- /dev/null +++ b/app/component/gallery/upload-pic/_upload-pic.scss @@ -0,0 +1 @@ +@import "../../../scss/lib/theme/vars"; diff --git a/app/component/gallery/upload-pic/upload-pic.html b/app/component/gallery/upload-pic/upload-pic.html new file mode 100644 index 00000000..7f7e8705 --- /dev/null +++ b/app/component/gallery/upload-pic/upload-pic.html @@ -0,0 +1,23 @@ +
    +
    + +

    upload a new pic

    + +
    + + +
    + +
    +

    + select a pic to upload +

    + + +
    +
    +
    diff --git a/app/component/gallery/upload-pic/upload-pic.js b/app/component/gallery/upload-pic/upload-pic.js new file mode 100644 index 00000000..7af96236 --- /dev/null +++ b/app/component/gallery/upload-pic/upload-pic.js @@ -0,0 +1,27 @@ +'use strict'; + +require('./_upload-pic.scss'); + +module.exports = { + template: require('./upload-pic.html'), + controller: ['$log', 'picService', UploadPicController], + controllerAs: 'uploadPicCtrl', + bindings: { + gallery: '<' + } +}; + +function UploadPicController($log, picService) { + $log.debug('UploadPicController'); + + this.pic = {}; + + this.uploadPic = function() { + picService.uploadGalleryPic(this.gallery, this.pic) + .then( () => { + this.pic.name = null; + this.pic.desc = null; + this.pic.file = null; + }) + } +} diff --git a/app/component/landing/login/_login.scss b/app/component/landing/login/_login.scss new file mode 100644 index 00000000..ba7410c5 --- /dev/null +++ b/app/component/landing/login/_login.scss @@ -0,0 +1 @@ +@import "../../../scss/lib/theme/vars"; diff --git a/app/component/landing/login/login.html b/app/component/landing/login/login.html new file mode 100644 index 00000000..d43a7bf4 --- /dev/null +++ b/app/component/landing/login/login.html @@ -0,0 +1,33 @@ + diff --git a/app/component/landing/login/login.js b/app/component/landing/login/login.js new file mode 100644 index 00000000..b7b47d14 --- /dev/null +++ b/app/component/landing/login/login.js @@ -0,0 +1,27 @@ +'use strict'; + +require('./_login.scss'); + +module.exports = { + template: require('./login.html'), + controller: ['$log', '$location', 'authService', LoginController], + controllerAs: 'loginCtrl' +}; + +function LoginController($log, $location, authService) { + $log.debug('LoginController'); + + authService.getToken() + .then( () => { + $location.url('/home'); + }); + + this.login = function() { + $log.log('loginCtrl.login()'); + + authService.login(this.user) + .then( () => { + $location.url('/home'); + }); + }; +}; diff --git a/app/component/landing/signup/_signup.scss b/app/component/landing/signup/_signup.scss new file mode 100644 index 00000000..ba7410c5 --- /dev/null +++ b/app/component/landing/signup/_signup.scss @@ -0,0 +1 @@ +@import "../../../scss/lib/theme/vars"; diff --git a/app/component/landing/signup/signup.html b/app/component/landing/signup/signup.html new file mode 100644 index 00000000..6f59c7e6 --- /dev/null +++ b/app/component/landing/signup/signup.html @@ -0,0 +1,25 @@ + diff --git a/app/component/landing/signup/signup.js b/app/component/landing/signup/signup.js new file mode 100644 index 00000000..25539d16 --- /dev/null +++ b/app/component/landing/signup/signup.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = { + template: require('./signup.html'), + controller: ['$log', '$location', 'authService', SignupController], + controllerAs: 'signupCtrl' +}; + +function SignupController($log, $location, authService) { + $log.debug('SignupController'); + + authService.getToken() + .then( () => { + $location.url('/home'); + }); + + this.signup = function(user) { + $log.debug('signupCtrl.signup()'); + + authService.signup(user) + .then( () => { + $location.url('/home'); + }); + }; +}; diff --git a/app/component/navbar/_navbar.scss b/app/component/navbar/_navbar.scss new file mode 100644 index 00000000..96861e8d --- /dev/null +++ b/app/component/navbar/_navbar.scss @@ -0,0 +1 @@ +@import "../../scss/lib/theme/vars"; diff --git a/app/component/navbar/navbar.html b/app/component/navbar/navbar.html new file mode 100644 index 00000000..cbb82c20 --- /dev/null +++ b/app/component/navbar/navbar.html @@ -0,0 +1,10 @@ + diff --git a/app/component/navbar/navbar.js b/app/component/navbar/navbar.js new file mode 100644 index 00000000..7e68d70a --- /dev/null +++ b/app/component/navbar/navbar.js @@ -0,0 +1,44 @@ +'use strict'; + +require('./_navbar.scss'); + +module.exports = { + template: require('./navbar.html'), + controller: ['$log', '$location', '$rootScope', 'authService', NavbarController], + controllerAs: 'navbarCtrl' +}; + +function NavbarController($log, $location, $rootScope, authService) { + $log.debug('NavbarController'); + + this.checkPath = function() { + let path = $location.path(); + if (path === '/join') { + this.hideButtons = true; + }; + + if (path !== '/join') { + this.hideButtons = false; + authService.getToken() + .catch( () => { + $location.url('/join#login'); + }); + }; + }; + + this.checkPath(); + + $rootScope.$on('$locationChangeSuccess', () => { + this.checkPath(); + }); + + this.logout = function() { + $log.log('navbarCtrl.logout()'); + this.hideButtons = true; + authService.logout() + .then( () => { + $location.url('/'); + }); + }; +}; + diff --git a/app/config/log-config.js b/app/config/log-config.js new file mode 100644 index 00000000..a23c5ad3 --- /dev/null +++ b/app/config/log-config.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = ['$logProvider', logConfig]; + +function logConfig($logProvider) { + $logProvider.debugEnabled(__DEBUG__); +}; + diff --git a/app/config/router-config.js b/app/config/router-config.js new file mode 100644 index 00000000..0fb3568a --- /dev/null +++ b/app/config/router-config.js @@ -0,0 +1,31 @@ +'use strict'; + +module.exports = ['$stateProvider', '$urlRouterProvider', routerConfig]; + +function routerConfig($stateProvider, $urlRouterProvider) { + $urlRouterProvider.when('' , '/join#signup'); + $urlRouterProvider.when('/' , '/join#signup'); + $urlRouterProvider.when('/signup' , '/join#signup'); + $urlRouterProvider.when('/login' , '/join#login'); + + let states = [ + { + name: 'home', + url: '/home', + template: require('../view/home/home.html'), + controller: 'HomeController', + controllerAs: 'homeCtrl' + }, + { + name: 'landing', + url: '/join', + template: require('../view/landing/landing.html'), + controller: 'LandingController', + controllerAs: 'landingCtrl' + }, + ]; + + states.forEach( state => { + $stateProvider.state(state); + }); +}; diff --git a/app/entry.js b/app/entry.js new file mode 100644 index 00000000..78ad727a --- /dev/null +++ b/app/entry.js @@ -0,0 +1,40 @@ +'use strict'; + +require('./scss/main.scss'); + +const path = require('path'); +const angular = require('angular'); +const camelcase = require('camelcase'); +const pascalcase = require('pascalcase'); +const uiRouter = require('angular-ui-router'); +const ngTouch = require('angular-touch'); +const ngAnimate = require('angular-animate'); +const ngFileUpload = require('ng-file-upload'); + +const cfgram = angular.module('cfgram', [ngTouch, ngAnimate, uiRouter, ngFileUpload]); + +let context = require.context('./config/', true, /\.js$/); +context.keys().forEach( path => { + cfgram.config(context(path)); +}); + +context = require.context('./view/', true, /\.js$/); +context.keys().forEach( key => { + let name = pascalcase(path.basename(key, '.js')); + let module = context(key); + cfgram.controller(name, module); +}); + +context = require.context('./service/', true, /\.js$/); +context.keys().forEach( key => { + let name = camelcase(path.basename(key, '.js')); + let module = context(key); + cfgram.service(name, module); +}); + +context = require.context('./component/', true, /\.js$/); +context.keys().forEach( key => { + let name = camelcase(path.basename(key, '.js')); + let module = context(key); + cfgram.component(name, module); +}); diff --git a/app/index.html b/app/index.html new file mode 100644 index 00000000..f4d84d6d --- /dev/null +++ b/app/index.html @@ -0,0 +1,19 @@ + + + + + + cfgram. + + +
    + +
    + +
    + +
    + +
    + + diff --git a/app/scss/lib/base/_base.scss b/app/scss/lib/base/_base.scss new file mode 100644 index 00000000..7dc9f4c5 --- /dev/null +++ b/app/scss/lib/base/_base.scss @@ -0,0 +1,74 @@ +html, body { + width: 100%; + height: 100%; + font-family: $font-std; + background: $brand-primary * 3; + color: $btn-primary; +} + +a { + text-decoration: none; + color: $black; + font-weight: 700; +} + +h1 { + font-size: 5vw; + margin-bottom: $gutter-sm; +} + +h2 { + font-size: 4vw; + margin: $gutter-sm / 2 0; +} + +h3 { + font-size: 3vw; + margin-bottom: $gutter-sm; +} + +button { + padding: 1% 5%; + background: $btn-primary; + color: $white; + font-size: 2vw; + border: none; + border-radius: $btn-radius; + cursor: pointer; + transition: 350ms all; +} + +.btn-std { + @extend button; +} + +input[type="text"] { + width: 100%; + padding: 1.5vw 2vw; + font-size: 2vw; + border: solid 1px $brand-primary; + border-radius: $btn-radius; + box-sizing: border-box; + + &:focus { + background: $brand-primary; + color: $white; + } +} + +input[type="password"] { + @extend input[type="text"]; +} + +input[disabled="disabled"] { + background: $disabled; +} + +.input-std { + @extend input[type="text"]; + display: inline-block; +} + +fieldset { + margin: 2% 0; +} diff --git a/app/scss/lib/base/_reset.scss b/app/scss/lib/base/_reset.scss new file mode 100644 index 00000000..8ec15d79 --- /dev/null +++ b/app/scss/lib/base/_reset.scss @@ -0,0 +1,61 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +::-webkit-input-placeholder { /* Chrome/Opera/Safari */ + color: #888; +} +::-moz-placeholder { /* Firefox 19+ */ + color: #888; +} +:-ms-input-placeholder { /* IE 10+ */ + color: #888; +} +:-moz-placeholder { /* Firefox 18- */ + color: #888; +} diff --git a/app/scss/lib/layout/_footer.scss b/app/scss/lib/layout/_footer.scss new file mode 100644 index 00000000..6294321e --- /dev/null +++ b/app/scss/lib/layout/_footer.scss @@ -0,0 +1,5 @@ +footer { + height: 10vw; + background: $brand-secondary; + margin-top: $gutter-std * 2; +} diff --git a/app/scss/lib/layout/_header.scss b/app/scss/lib/layout/_header.scss new file mode 100644 index 00000000..a86b8611 --- /dev/null +++ b/app/scss/lib/layout/_header.scss @@ -0,0 +1,4 @@ +header { + height: 8vw; + background: $brand-primary; +} diff --git a/app/scss/lib/layout/_layout.scss b/app/scss/lib/layout/_layout.scss new file mode 100644 index 00000000..cb94f019 --- /dev/null +++ b/app/scss/lib/layout/_layout.scss @@ -0,0 +1,5 @@ +main { + width: 90%; + margin: 5%; + min-height: 600px; +} diff --git a/app/scss/lib/theme/_vars.scss b/app/scss/lib/theme/_vars.scss new file mode 100644 index 00000000..13c8e263 --- /dev/null +++ b/app/scss/lib/theme/_vars.scss @@ -0,0 +1,11 @@ +$brand-primary: #444; +$brand-secondary: $brand-primary * 2; +$btn-primary: $brand-primary / 2; +$btn-radius: 3px; +$font-std: helvetica; +$font-secondary: monospace; +$white: #fff; +$black: #000; +$disabled: #e3e3e3; +$gutter-std: 5%; +$gutter-sm: $gutter-std / 2; diff --git a/app/scss/main.scss b/app/scss/main.scss new file mode 100644 index 00000000..e3e96d4d --- /dev/null +++ b/app/scss/main.scss @@ -0,0 +1,22 @@ +// :::: BASE & THEME :::: // +@import "lib/base/reset"; +@import "lib/theme/vars"; +@import "lib/base/base"; + +// :::: LAYOUT :::: // +@import "lib/layout/header"; +@import "lib/layout/footer"; +@import "lib/layout/layout"; + +// :::: VIEW :::: // +@import "../view/landing/landing"; + +// :::: COMPONENT :::: // +@import "../component/landing/login/login"; +@import "../component/landing/signup/signup"; +@import "../component/gallery/create-gallery/create-gallery"; +@import "../component/gallery/edit-gallery/edit-gallery"; +@import "../component/gallery/gallery-item/gallery-item"; +@import "../component/gallery/thumbnail-container/thumbnail-container"; +@import "../component/gallery/thumbnail/thumbnail"; +@import "../component/gallery/upload-pic/upload-pic"; diff --git a/app/service/auth-service.js b/app/service/auth-service.js new file mode 100644 index 00000000..4dd09087 --- /dev/null +++ b/app/service/auth-service.js @@ -0,0 +1,88 @@ +'use strict'; + +module.exports = ['$q', '$log', '$http', '$window', authService]; + +function authService($q, $log, $http, $window){ + $log.debug('authService'); + + let service = {}; + let token = null; + + function setToken(_token){ + $log.debug('authService.setToken()'); + + if (! _token) { + return $q.reject(new Error('no token')); + }; + + $window.localStorage.setItem('token', _token); + token = _token; + return $q.resolve(token); + } + + service.getToken = function(){ + $log.debug('authService.getToken'); + if (token) { + return $q.resolve(token); + }; + + token = $window.localStorage.getItem('token'); + if (token) return $q.resolve(token); + return $q.reject(new Error('token not found')); + }; + + service.logout = function(){ + $log.debug('authService.logout()'); + + $window.localStorage.removeItem('token'); + token = null; + return $q.resolve(); + }; + + service.signup = function(user) { + $log.debug('authService.signup()'); + + let url = `${__API_URL__}/api/signup`; + let config = { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + }; + + return $http.post(url, user, config) + .then( res => { + $log.log('success', res.data); + return setToken(res.data); + }) + .catch(err => { + $log.error('failure', err.message); + return $q.reject(err); + }); + }; + + service.login = function(user){ + $log.debug('authService.login()'); + + let url = `${__API_URL__}/api/login`; + let base64 = $window.btoa(`${user.username}:${user.password}`); + let config = { + headers: { + Accept: 'application/json', + Authorization: `Basic ${base64}`, + } + }; + + return $http.get(url, config) + .then( res => { + $log.log('success', res.data); + return setToken(res.data); + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + return service; +} diff --git a/app/service/gallery-service.js b/app/service/gallery-service.js new file mode 100644 index 00000000..9969ad01 --- /dev/null +++ b/app/service/gallery-service.js @@ -0,0 +1,141 @@ +'use strict'; + +module.exports = ['$q', '$log', '$http', 'authService', galleryService]; + +function galleryService($q, $log, $http, authService) { + $log.debug('galleryService()'); + + let service = {}; + service.galleries = []; + + service.createGallery = function(gallery) { + $log.debug('galleryService.createGallery()'); + + return authService.getToken() + .then( token => { + let url = `${__API_URL__}/api/gallery`; + let config = { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }; + + return $http.post(url, gallery, config); + }) + .then( res => { + $log.log('gallery created'); + let gallery = res.data; + service.galleries.unshift(gallery); + return gallery; + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + service.deleteGalleries = function(galleryID, galleryData) { + return authService.getToken() + .then( token => { + let url = `${__API_URL__}/api/gallery/${galleryID}`; + let config = { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + }; + }); + }; + + service.fetchGalleries = function() { + $log.debug('galleryService.fetchGalleries()'); + + return authService.getToken() + .then( token => { + let url = `${__API_URL__}/api/gallery`; + let config = { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + }; + + return $http.get(url, config); + }) + .then( res => { + $log.log('galleries retrieved'); + service.galleries = res.data; + return service.galleries; + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + service.updateGallery = function(galleryID, galleryData) { + $log.debug('galleryService.updateGallery()'); + + return authService.getToken() + .then( token => { + let url = `${__API_URL__}/api/gallery/${galleryID}`; + let config = { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + + return $http.put(url, galleryData, config); + }) + .then( res => { + for (let i = 0; i < service.galleries.length; i++) { + let current = service.galleries[i]; + if (current._id === galleryID) { + service.galleries[i] = res.data; + break; + }; + }; + + return res.data; + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + service.deleteGallery = function(galleryID) { + $log.debug('galleryService.updateGallery()'); + + return authService.getToken() + .then( token => { + let url = `${__API_URL__}/api/gallery/${galleryID}`; + let config = { + headers: { + Authorization: `Bearer ${token}` + } + }; + + return $http.delete(url, config); + }) + .then( res => { + for (let i = 0; i < service.galleries.length; i++) { + let current = service.galleries[i]; + if (current._id === galleryID) { + service.galleries.splice(i, 1); + break; + }; + }; + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + return service; +}; diff --git a/app/service/pic-service.js b/app/service/pic-service.js new file mode 100644 index 00000000..96f5cfe9 --- /dev/null +++ b/app/service/pic-service.js @@ -0,0 +1,43 @@ +'use strict'; + +module.exports = ['$q', '$log', '$http', 'Upload', 'authService', picService]; + +function picService($q, $log, $http, Upload, authService) { + $log.debug('picService'); + + let service = {}; + + service.uploadGalleryPic = function(galleryData, picData) { + $log.debug('uploadGalleryPic'); + + return authService.getToken() + .then( token => { + let url = `${__API_URL__}/api/gallery/${galleryData._id}/pic`; + let headers = { + Authorization: `Bearer ${token}`, + Accept: 'application/json' + }; + + return Upload.upload({ + url, + headers, + method: 'POST', + data: { + name: picData.name, + desc: picData.desc, + file: picData.file + } + }); + }) + .then( res => { + galleryData.pics.unshift(res.data); + return res.data; + }) + .catch( err => { + $log.error(err.message); + return $q.reject(err); + }); + }; + + return service; +}; diff --git a/app/view/home/_home.scss b/app/view/home/_home.scss new file mode 100644 index 00000000..e69de29b diff --git a/app/view/home/home-controller.js b/app/view/home/home-controller.js new file mode 100644 index 00000000..1e5dfb11 --- /dev/null +++ b/app/view/home/home-controller.js @@ -0,0 +1,31 @@ +'use strict'; + +require('./_home.scss'); + +module.exports = ['$log', '$rootScope', 'galleryService', HomeController]; + +function HomeController($log, $rootScope, galleryService) { + $log.debug('HomeController'); + + this.galleries = []; + + this.fetchGalleries = function() { + galleryService.fetchGalleries() + .then( galleries => { + this.galleries = galleries; + this.currentGallery = galleries[0]; + }); + }; + + this.galleryDeleteDone = function(gallery) { + if (this.currentGallery._id === gallery._id) { + this.currentGallery = null; + } + } + + this.fetchGalleries(); + + $rootScope.$on('$locationChangeSuccess', () => { + this.fetchGalleries(); + }); +}; diff --git a/app/view/home/home.html b/app/view/home/home.html new file mode 100644 index 00000000..71cdefe8 --- /dev/null +++ b/app/view/home/home.html @@ -0,0 +1,16 @@ +
    + + + + + +
    diff --git a/app/view/landing/_landing.scss b/app/view/landing/_landing.scss new file mode 100644 index 00000000..96861e8d --- /dev/null +++ b/app/view/landing/_landing.scss @@ -0,0 +1 @@ +@import "../../scss/lib/theme/vars"; diff --git a/app/view/landing/landing-controller.js b/app/view/landing/landing-controller.js new file mode 100644 index 00000000..d62d6772 --- /dev/null +++ b/app/view/landing/landing-controller.js @@ -0,0 +1,11 @@ +'use strict'; + +require('./_landing.scss'); + +module.exports = ['$log', '$location', '$rootScope', 'authService', LandingController]; + +function LandingController($log, $location, authService) { + let url = $location.url(); + this.showSignup = url === '/join#signup' || url === '/join'; +}; + diff --git a/app/view/landing/landing.html b/app/view/landing/landing.html new file mode 100644 index 00000000..ec36e4c7 --- /dev/null +++ b/app/view/landing/landing.html @@ -0,0 +1,29 @@ +
    +
    +
    + +
    +

    already a member?

    + + sign in here. + +
    +
    +
    + +
    +
    + +
    +

    want to signup?

    + + sign up here. + +
    +
    +
    +
    diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 00000000..cfa0bbfe --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,75 @@ +// Karma configuration +// Generated on Tue Apr 04 2017 10:16:51 GMT-0700 (PDT) +const webpack = require('./webpack.config.js'); +delete webpack.entry; + +module.exports = function(config) { + config.set({ + webpack, + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jasmine'], + + + // list of files / patterns to load in the browser + files: [ + 'app/entry.js', + 'test/**/*-test.js', + 'node_modules/angular-mocks/angular-mocks.js' + ], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'test/**/*-test.js': ['webpack'], + 'app/entry.js': ['webpack'] + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['mocha'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..850ec2b3 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "cfgram-auth", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "./node_modules/webpack/bin/webpack.js", + "build-watch": "./node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot", + "test": "./node_modules/karma/bin/karma start --single-run", + "test-watch": "./node_modules/karma/bin/karma start" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "angular": "^1.6.3", + "angular-animate": "^1.6.3", + "angular-route": "^1.6.3", + "angular-touch": "^1.6.3", + "angular-ui-router": "^0.4.2", + "babel-core": "^6.24.0", + "babel-loader": "^6.4.1", + "babel-preset-es2015": "^6.24.0", + "camelcase": "^4.0.0", + "clean-webpack-plugin": "^0.1.16", + "css-loader": "^0.27.3", + "dotenv": "^4.0.0", + "extract-text-webpack-plugin": "^2.1.0", + "file-loader": "^0.10.1", + "html-loader": "^0.4.5", + "html-webpack-plugin": "^2.28.0", + "ng-file-upload": "^12.2.13", + "node-sass": "^4.5.1", + "pascalcase": "^0.1.1", + "resolve-url-loader": "^2.0.2", + "sass-loader": "^6.0.3", + "style-loader": "^0.16.1", + "ui-router": "^1.0.0-alpha.3", + "url-loader": "^0.5.8", + "webpack": "^2.3.2" + }, + "devDependencies": { + "angular-mocks": "^1.6.4", + "jasmine-core": "^2.5.2", + "karma": "^1.5.0", + "karma-chrome-launcher": "^2.0.0", + "karma-jasmine": "^1.1.0", + "karma-mocha-reporter": "^2.2.3", + "karma-webpack": "^2.0.3", + "webpack-dev-server": "^2.4.2" + } +} diff --git a/test/auth-service-test.js b/test/auth-service-test.js new file mode 100644 index 00000000..e29956f5 --- /dev/null +++ b/test/auth-service-test.js @@ -0,0 +1,23 @@ +'use strict'; + +describe('Auth Service', function() { + beforeEach(() => { + angular.mock.module('cfgram'); + angular.mock.inject(($rootScope, authService, $window, $httpBackend) => { + this.$rootScope = $rootScope; + this.authService = authService; + this.$httpBackend = $httpBackend; + this.$window = $window; + }); + }); + describe('authService.getToken', () => { + it('should return a token', () => { + this.authService.token = null; + this.$window.localStorage.setItem('token', 'test token'); + this.authService.getToken() + .then(token => expect(token).toEqual('test token')) + .catch(err => expect(err).toEqual(null)); + this.$rootScope.$apply(); + }); + }); +}); diff --git a/test/edit-gallery-component-test.js b/test/edit-gallery-component-test.js new file mode 100644 index 00000000..5fb5934a --- /dev/null +++ b/test/edit-gallery-component-test.js @@ -0,0 +1,48 @@ +'use strict'; + +describe('Edit Gallery Component', function() { + beforeEach(() => { + angular.mock.module('cfgram'); + angular.mock.inject(($rootScope, $componentController, $httpBackend, authService) => { + this.$rootScope = $rootScope; + this.$componentController = $componentController; + this.$httpBackend = $httpBackend; + this.authService = authService; + }); + }); + it('should contain the proper component bindings', () => { + let bindings = { + gallery: { + name: 'test gallery name', + desc: 'test gallery description' + } + }; + let editGalleryCtrl = this.$componentController('editGallery', null, bindings); + expect(editGalleryCtrl.gallery.name).toEqual(bindings.gallery.name); + expect(editGalleryCtrl.gallery.desc).toEqual(bindings.gallery.desc); + this.$rootScope.$apply(); + }); + describe('editGalleryCtrl.updateGallery()', () => { + it('should make a valid PUT request', () => { + let url = 'http://localhost:3000/api/gallery/7'; + let headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: 'Bearer test token' + }; + let tempGallery = { + _id: '7', + name: 'updated name', + desc: 'updated desc' + }; + this.$httpBackend.expectPUT(url, tempGallery, headers).respond(200); + + let bindings = { gallery: tempGallery }; + let editGalleryCtrl = this.$componentController('editGallery', null, bindings); + editGalleryCtrl.updateGallery(); + expect(editGalleryCtrl.gallery.name).toEqual('updated name'); + expect(editGalleryCtrl.gallery.desc).toEqual('updated desc'); + this.$rootScope.$apply(); + }); + }) +}); diff --git a/test/gallery-item-component-test.js b/test/gallery-item-component-test.js new file mode 100644 index 00000000..ec2e48f2 --- /dev/null +++ b/test/gallery-item-component-test.js @@ -0,0 +1,82 @@ +'use strict'; + +describe('Gallery Item Component', function() { + beforeEach(() => { + angular.mock.module('cfgram'); + angular.mock.inject(($rootScope, $componentController, $httpBackend, authService) => { + this.$rootScope = $rootScope; + this.$componentController = $componentController; + this.$httpBackend = $httpBackend; + this.authService = authService; + }); + }); + describe('galleryItemCtrl.deleteDone()', () => { + it('should call deleteDone', () => { + let bindings = { + gallery: { + _id: '7', + name: 'test gallery name', + desc: 'test gallery description', + pics: [], + }, + deleteDone: function(data) { + expect(data.galleryData._id).toEqual('7'); + } + }; + let galleryItemCtrl = this.$componentController('galleryItem', null, bindings); + galleryItemCtrl.deleteDone({ galleryData: galleryItemCtrl.gallery }); + + this.$rootScope.$apply(); + }); + }); + it('should call deleteDone with gallery after deleteGallery', () => { + let url = 'http://localhost:3000/api/gallery/7'; + let headers = { + Authorization: 'Bearer test token', + Accept: 'application/json, text/plain, */*' + }; + let tempGallery = { + _id: '7', + name: 'test name', + desc: 'test desc' + }; + let bindings = { + gallery: tempGallery, + deleteDone: function(data) { + expect(data.galleryData._id).toEqual('7'); + } + }; + this.$httpBackend.expectDELETE(url, headers).respond(204); + let galleryItemCtrl = this.$componentController('galleryItem', null, bindings); + // spyOn(galleryItemCtrl, 'deleteDone'); + galleryItemCtrl.deleteGallery('7'); + // expect(galleryItemCtrl.deleteDone).toHaveBeenCalledWith('7'); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + }); + describe('galleryItemCtrl.deleteGallery()', () => { + it('should make a valid DELETE request', () => { + let url = 'http://localhost:3000/api/gallery/7'; + let headers = { + Authorization: 'Bearer test token', + Accept: 'application/json, text/plain, */*' + }; + let tempGallery = { + _id: '7', + name: 'test name', + desc: 'test desc' + }; + let bindings = { + gallery: tempGallery, + deleteDone: function(data) { + expect(data.galleryData._id).toEqual('7'); + } + }; + this.$httpBackend.expectDELETE(url, headers).respond(204); + let galleryItemCtrl = this.$componentController('galleryItem', null, bindings); + galleryItemCtrl.deleteGallery(tempGallery._id) + this.$httpBackend.flush(); + this.$rootScope.$apply(); + }); +}); +}); diff --git a/test/gallery-service-test.js b/test/gallery-service-test.js new file mode 100644 index 00000000..95c2f466 --- /dev/null +++ b/test/gallery-service-test.js @@ -0,0 +1,87 @@ +'use strict'; + +describe('Gallery Service', function() { + beforeEach(() => { + angular.mock.module('cfgram'); + angular.mock.inject(($rootScope, authService, galleryService, $window, $httpBackend) => { + this.$rootScope = $rootScope; + this.authService = authService; + this.galleryService = galleryService; + this.$httpBackend = $httpBackend; + this.$window = $window; + }); + }); + describe('galleryService.createGallery', () => { + it('should create a new gallery', () => { + let galleryData = { + name: 'example gallery name', + desc: 'example gallery description' + }; + let headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: 'Bearer test token' + }; + this.$httpBackend.expectPOST('http://localhost:3000/api/gallery', galleryData, headers) + .respond(200, { + _id: '1', + username: 'exampleuser', + name: galleryData.name, + desc: galleryData.desc, + pics: [] + }); + this.galleryService.createGallery(galleryData); + this.$httpBackend.flush(); + this.$rootScope.$apply(); + }); + }); + describe('galleryService.fetchGalleries', () => { + it('should get a gallery', () => { + let headers = { + Accept: 'application/json', + Authorization: 'Bearer test token' + }; + this.$httpBackend.expectGET('http://localhost:3000/api/gallery', headers) + .respond(200, { + _id: '1', + username: 'exampleuser', + name: 'example gallery name', + desc: 'example gallery description', + pics: [] + }); + this.$rootScope.$apply(); + }); + }); + describe('galleryService.updateGallery', () => { + it('should update a gallery', () => { + let galleryData = { + name: 'new gallery name', + desc: 'new gallery description' + }; + let config = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: 'Bearer test token' + } + }; + this.$httpBackend.expectPUT('http://localhost:3000/api/gallery/', galleryData, config) + .respond(200, { + _id: '1', + username: 'exampleuser', + name: 'new gallery name', + desc: 'new gallery description', + pics: [] + }); + this.$rootScope.$apply(); + }); + }); + describe('galleryService.deleteGallery', () => { + it('should delete a gallery', () => { + this.$httpBackend.expectDELETE('http://localhost:3000/api/gallery/1') + .respond(204); + this.galleryService.deleteGallery('1'); + this.$rootScope.$apply(); + }); + }); +}); diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..88737943 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,46 @@ +'use strict'; + +const dotenv = require('dotenv'); +const webpack = require('webpack'); +const HTMLPlugin = require('html-webpack-plugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +const production = process.env.NODE_ENV === 'production'; + +dotenv.load(); + +module.exports = { + devtool: 'eval', + entry: `${__dirname}/app/entry.js`, + output: { + filename: 'bundle.js', + path: `${__dirname}/build` + }, + plugins: [ + new HTMLPlugin({ + template: `${__dirname}/app/index.html` + }), + new ExtractTextPlugin('bundle.css'), + new webpack.DefinePlugin({ + __API_URL__: JSON.stringify(process.env.API_URL), + __DEBUG__: JSON.stringify(!production) + }) + ], + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }, + { + test: /\.html$/, + loader: 'html-loader' + }, + { + test: /\.scss$/, + loader: ExtractTextPlugin.extract(['css-loader', 'sass-loader']) + } + ] + } +}; diff --git a/wireframes/assets/cf-logo.png b/wireframes/assets/cf-logo.png deleted file mode 100644 index 38a23b2d..00000000 Binary files a/wireframes/assets/cf-logo.png and /dev/null differ diff --git a/wireframes/signin.png b/wireframes/signin.png deleted file mode 100644 index 98280c96..00000000 Binary files a/wireframes/signin.png and /dev/null differ diff --git a/wireframes/signup.png b/wireframes/signup.png deleted file mode 100644 index c52936c7..00000000 Binary files a/wireframes/signup.png and /dev/null differ