diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c71d366 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "plugins": ["transform-runtime"], + "presets": ["es2015", "stage-0"] +} diff --git a/.gitignore b/.gitignore index 8320361..c828994 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /nbproject/ /node_modules/ -/dist/ +/build/ +!/build/jquery-1.6.2.min.js +!/build/jquery.event.drag-2.0.js \ No newline at end of file diff --git a/app.js b/app.js index 2a5b35f..76eb28b 100644 --- a/app.js +++ b/app.js @@ -13,7 +13,7 @@ const app = express(); const server = http.createServer(app); const io = require('socket.io').listen(server); -app.set('views', path.join(__dirname, 'views')); +//app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); server.listen(8085, function () { @@ -27,13 +27,14 @@ app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: false})); app.use(cookieParser()); -app.use(express.static(path.join(__dirname, 'public'))); +//app.use(express.static(path.join(__dirname, 'public'))); +//app.use(express.static('./views/')); +//app.use('/*', express.static('./views/index.ejs')); app.use('/', routes); app.use('/users', users); - io.on('connection', function (socket) { socket.emit('test', 'Connectedddd'); socket.on('drawClick', function (data) { diff --git a/gulpfile.babel.js b/gulpfile.babel.js new file mode 100644 index 0000000..eb8ae7e --- /dev/null +++ b/gulpfile.babel.js @@ -0,0 +1,112 @@ +'use strict'; +import gulp from 'gulp'; +import nodemon from 'gulp-nodemon'; +import browserSync from 'browser-sync'; +import del from 'del'; +import source from 'vinyl-source-stream'; +import browserify from 'browserify'; +import babelify from 'babelify'; +import ngAnnotate from 'browserify-ngannotate'; +import templateCache from 'gulp-angular-templatecache'; +import rename from 'gulp-rename'; +import notify from 'gulp-notify'; +import ejs from 'gulp-ejs'; +import htmlreplace from 'gulp-html-replace'; +import concat from 'gulp-concat'; +import minifyCSS from 'gulp-minify-css'; + +// Where our files are located +var jsFiles = "public/assets/javascripts/*.js"; +var viewFiles = "views/*.ejs"; + +var interceptErrors = function (error) { + var args = Array.prototype.slice.call(arguments); + + // Send error to notification center with gulp-notify + notify.onError({ + title: 'Compile Error', + message: '<%= error.message %>' + }).apply(this, args); + + // Keep gulp from hanging on this task + this.emit('end'); +}; + +gulp.task('default', ['build', 'browser-sync'], function () { + gulp.watch(['./views/**.**', './public/**/*'], ['build']); + gulp.watch('./build/', browserSync.reload()); +}); + +gulp.task('browser-sync', ['nodemon'], function () { + browserSync.init(['./build/**.**'], { + port: 8080, + serveStatic: ['./build/'], + proxy: { + target: 'localhost:8085', // original port + ws: true // enables websockets + } + }); +}); + +gulp.task('nodemon', (cb) => { + var started = false; + + return nodemon({ + script: 'app.js' + }).on('start', function () { + if (!started) { + cb(); + started = true; + } + }); +}); + +gulp.task('build', ['browserify', 'css'], () => { + gulp.start('html'); + //gulp.start() will have to be replaced by gulp.series() when gulp 4.0 is released. +}); + +gulp.task('browserify', ['views'], function () { + return browserify(['./public/assets/javascripts/main.js']) + .transform(babelify, {presets: ["es2015"]}) + .transform(ngAnnotate) + .bundle() + .on('error', interceptErrors) + .pipe(source('main.js')) + .pipe(gulp.dest('./build/')); +}); + +gulp.task('views', function () { + return gulp.src(viewFiles) + .pipe(templateCache({ + standalone: true + })) + .on('error', interceptErrors) + .pipe(rename("app.templates.js")) + .pipe(gulp.dest('./public/config/')); +}); + +gulp.task('css', function () { + gulp.src('./public/assets/stylesheets/*.css') + .pipe(minifyCSS()) + .pipe(concat('style.css')) + .on('error', interceptErrors) + .pipe(gulp.dest('./build')) +}); + +gulp.task('html', function () { + return gulp.src("./views/index.ejs") + .pipe(ejs({}, {ext: '.html'})) + .pipe(htmlreplace({ + css: 'style.css', + js: 'main.js' + })) + .on('error', interceptErrors) + .pipe(gulp.dest('./build/')); +}); + +gulp.task('clean', () => { + del(['./build/**', '!./build', '!./build/jquery-1.6.2.min.js', '!./build/jquery.event.drag-2.0.js', '!./build/socket.io.js']).then(paths => { + console.log('Deleted files and folders:\n', paths.join('\n')); + }); +}); diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index d10fec5..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict'; - -const gulp = require('gulp'), - gulpLoadPlugins = require('gulp-load-plugins'), - plugins = gulpLoadPlugins(), - del = require('del'), - eslint = require('gulp-eslint'); - -gulp.task('scripts', function() { - return gulp.src('public/javascripts/*.js') - .pipe(plugins.concat('main.js')) - .pipe(gulp.dest('dist/assets/js')) - console.log("Scripts task complete"); -}); - -gulp.task('clean', function() { - return del(['dist/assets/css', 'dist/assets/js', 'dist/assets/img']); -}); - -gulp.task('default', ['clean', 'lint'], function() { - gulp.start('scripts'); -}); - -gulp.task('watch', function() { - //gulp.watch('public/stylesheets/**/*.css', ['styles']); - gulp.watch('public/javascripts/*.js', ['scripts']); - plugins.livereload.listen(); - gulp.watch(['./**']).on('change', plugins.livereload.changed); -}); - -gulp.task('lint', () => { - return gulp.src(['**/*.js','!node_modules/**']) - .pipe(eslint('.eslintrc.json')) - .pipe(eslint.formatEach()); -}); \ No newline at end of file diff --git a/package.json b/package.json index 16a6eb6..5020b92 100644 --- a/package.json +++ b/package.json @@ -18,23 +18,40 @@ }, "homepage": "https://github.com/gevalo1/WebApps3#readme", "devDependencies": { + "babel-core": "^6.18.2", + "babel-loader": "^6.2.7", + "babel-plugin-transform-runtime": "^6.15.0", + "babel-polyfill": "^6.16.0", + "babel-preset-es2015": "^6.18.0", + "babel-preset-stage-0": "^6.16.0", + "babelify": "^7.3.0", + "browser-sync": "^2.17.5", + "browserify": "^13.1.1", + "browserify-ngannotate": "^2.0.0", "del": "^2.2.2", "eslint": "^3.9.0", "eslint-config-standard": "^6.2.1", "eslint-plugin-promise": "^3.3.0", "eslint-plugin-standard": "^2.0.1", "gulp": "^3.9.1", - "gulp-autoprefixer": "^3.1.1", + "gulp-angular-templatecache": "^2.0.0", "gulp-concat": "^2.6.0", - "gulp-cssnano": "^2.1.2", - "gulp-livereload": "^3.8.1", - "gulp-load-plugins": "^1.3.0", - "gulp-rename": "^1.2.2" + "gulp-ejs": "^2.2.1", + "gulp-html-replace": "^1.6.1", + "gulp-minify-css": "^1.2.4", + "gulp-nodemon": "^2.2.1", + "gulp-notify": "^2.2.0", + "gulp-rename": "^1.2.2", + "gulp-uglify": "^2.0.0", + "vinyl-source-stream": "^1.1.0" }, "dependencies": { - "ejs": "2.5.2", + "angular": "^1.5.8", + "angular-ui-router": "^0.3.1", + "babel-runtime": "^6.18.0", "body-parser": "1.15.2", "cookie-parser": "1.4.3", + "ejs": "2.5.2", "express": "4.14.0", "morgan": "1.7.0", "serve-favicon": "2.3.0", diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..9a44a0c --- /dev/null +++ b/public/app.js @@ -0,0 +1,34 @@ +import angular from 'angular'; + +// Import our app config files +import constants from './config/app.constants'; +import appConfig from './config/app.config'; +import appRun from './config/app.run'; +import 'angular-ui-router'; +// Import our templates file (generated by Gulp) +import './config/app.templates'; +// Import our app functionaity +import './components'; +import './services'; +import './auth'; + + +// Create and bootstrap application +const requires = [ + 'app.components', + 'app.services', + 'app.auth', +]; + +// Mount on window for testing +window.app = angular.module('app', requires); + +angular.module('app').constant('AppConstants', constants); + +angular.module('app').config(appConfig); + +angular.module('app').run(appRun); + +angular.bootstrap(document, ['app'], { + strictDi: true +}); diff --git a/public/app/auth/auth.config.js b/public/app/auth/auth.config.js new file mode 100644 index 0000000..04f2d25 --- /dev/null +++ b/public/app/auth/auth.config.js @@ -0,0 +1,33 @@ +function AuthConfig($stateProvider, $httpProvider) { + 'ngInject'; + + // Define the routes + $stateProvider + + .state('app.login', { + url: '/login', + controller: 'AuthCtrl as $ctrl', + templateUrl: 'auth/auth.html', + title: 'Sign in', + resolve: { + auth: function(User) { + return User.ensureAuthIs(false); + } + } + }) + + .state('app.register', { + url: '/register', + controller: 'AuthCtrl as $ctrl', + templateUrl: 'auth/auth.html', + title: 'Sign up', + resolve: { + auth: function(User) { + return User.ensureAuthIs(false); + } + } + }); + +}; + +export default AuthConfig; \ No newline at end of file diff --git a/public/app/auth/auth.controller.js b/public/app/auth/auth.controller.js new file mode 100644 index 0000000..bcf974b --- /dev/null +++ b/public/app/auth/auth.controller.js @@ -0,0 +1,27 @@ +class AuthCtrl { + constructor(User, $state) { + 'ngInject'; + + this._User = User; + this._$state = $state; + + this.title = $state.current.title; + this.authType = $state.current.name.replace('app.', ''); + } + + submitForm() { + this.isSubmitting = true; + + this._User.attemptAuth(this.authType, this.formData).then( + (res) => { + this._$state.go('app.home'); + }, + (err) => { + this.isSubmitting = false; + this.errors = err.data.errors; + } + ); + } +} + +export default AuthCtrl; \ No newline at end of file diff --git a/public/app/auth/auth.html b/public/app/auth/auth.html new file mode 100644 index 0000000..aafdd8e --- /dev/null +++ b/public/app/auth/auth.html @@ -0,0 +1,52 @@ +
+
+
+ +
+

+

+ + Have an account? + + + Need an account? + +

+ + + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/public/app/auth/index.js b/public/app/auth/index.js new file mode 100644 index 0000000..5838d03 --- /dev/null +++ b/public/app/auth/index.js @@ -0,0 +1,15 @@ +import angular from 'angular'; + +// Create the home module where our functionality can attach to +let authModule = angular.module('app.auth', []); + + +// Include our UI-Router config settings +import AuthConfig from './auth.config'; +authModule.config(AuthConfig); + +// Controllers +import AuthCtrl from './auth.controller'; +authModule.controller('AuthCtrl', AuthCtrl); + +export default authModule; \ No newline at end of file diff --git a/public/app/components/article-helpers/article-list.component.js b/public/app/components/article-helpers/article-list.component.js new file mode 100644 index 0000000..1a79a6a --- /dev/null +++ b/public/app/components/article-helpers/article-list.component.js @@ -0,0 +1,68 @@ +class ArticleListCtrl { + constructor(Articles, $scope) { + 'ngInject'; + + this._Articles = Articles; + + this.setListTo(this.listConfig); + + $scope.$on('setListTo', (ev, newList) => { + this.setListTo(newList); + }); + + $scope.$on('setPageTo', (ev, pageNumber) => { + this.setPageTo(pageNumber); + }) + } + + setListTo(newList) { + this.list = []; + + this.listConfig = newList; + + this.runQuery(); + } + + setPageTo(pageNumber) { + this.listConfig.currentPage = pageNumber; + + this.runQuery(); + } + + runQuery() { + this.loading = true; + + let queryConfig = { + type: this.listConfig.type, + filters: this.listConfig.filters || {} + }; + + queryConfig.filters.limit = this.limit; + + if (!this.listConfig.currentPage) { + this.listConfig.currentPage = 1; + } + + queryConfig.filters.offset = (this.limit * (this.listConfig.currentPage - 1)); + + this._Articles.query(queryConfig).then( + (res) => { + this.loading = false; + this.list = res.articles; + + this.listConfig.totalPages = Math.ceil(res.articlesCount / this.limit); + } + ); + } +} + +let ArticleList = { + bindings: { + limit: '=', + listConfig: '=' + }, + controller: ArticleListCtrl, + templateUrl: 'components/article-helpers/article-list.html' +}; + +export default ArticleList; \ No newline at end of file diff --git a/public/app/components/article-helpers/article-list.html b/public/app/components/article-helpers/article-list.html new file mode 100644 index 0000000..1f82eed --- /dev/null +++ b/public/app/components/article-helpers/article-list.html @@ -0,0 +1,20 @@ + + + +
+ Loading articles... +
+ +
+ No articles are here... yet. +
+ + + \ No newline at end of file diff --git a/public/app/components/article-helpers/article-meta.component.js b/public/app/components/article-helpers/article-meta.component.js new file mode 100644 index 0000000..1dca631 --- /dev/null +++ b/public/app/components/article-helpers/article-meta.component.js @@ -0,0 +1,9 @@ +let ArticleMeta = { + bindings: { + article: '=' + }, + transclude: true, + templateUrl: 'components/article-helpers/article-meta.html' +}; + +export default ArticleMeta; \ No newline at end of file diff --git a/public/app/components/article-helpers/article-meta.html b/public/app/components/article-helpers/article-meta.html new file mode 100644 index 0000000..ccc3702 --- /dev/null +++ b/public/app/components/article-helpers/article-meta.html @@ -0,0 +1,17 @@ +
+ + + + +
+ + + + +
+ + +
\ No newline at end of file diff --git a/public/app/components/article-helpers/article-preview.component.js b/public/app/components/article-helpers/article-preview.component.js new file mode 100644 index 0000000..6a36190 --- /dev/null +++ b/public/app/components/article-helpers/article-preview.component.js @@ -0,0 +1,8 @@ +let ArticlePreview = { + bindings: { + article: '=' + }, + templateUrl: 'components/article-helpers/article-preview.html' +}; + +export default ArticlePreview; \ No newline at end of file diff --git a/public/app/components/article-helpers/article-preview.html b/public/app/components/article-helpers/article-preview.html new file mode 100644 index 0000000..3bc6d0b --- /dev/null +++ b/public/app/components/article-helpers/article-preview.html @@ -0,0 +1,21 @@ +
+ + + {{$ctrl.article.favoritesCount}} + + + + +

+

+ Read more... + +
+
\ No newline at end of file diff --git a/public/app/components/article-helpers/list-pagination.component.js b/public/app/components/article-helpers/list-pagination.component.js new file mode 100644 index 0000000..37eea7f --- /dev/null +++ b/public/app/components/article-helpers/list-pagination.component.js @@ -0,0 +1,33 @@ +class ListPaginationCtrl { + constructor($scope) { + 'ngInject'; + + this._$scope = $scope; + + } + + pageRange(total) { + let pages = []; + + for (var i = 0; i < total; i++) { + pages.push(i + 1) + } + + return pages; + } + + changePage(number) { + this._$scope.$emit('setPageTo', number); + } +} + +let ListPagination= { + bindings: { + totalPages: '=', + currentPage: '=' + }, + controller: ListPaginationCtrl, + templateUrl: 'components/article-helpers/list-pagination.html' +}; + +export default ListPagination; \ No newline at end of file diff --git a/public/app/components/article-helpers/list-pagination.html b/public/app/components/article-helpers/list-pagination.html new file mode 100644 index 0000000..39ebf6c --- /dev/null +++ b/public/app/components/article-helpers/list-pagination.html @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/public/app/components/buttons/favorite-btn.component.js b/public/app/components/buttons/favorite-btn.component.js new file mode 100644 index 0000000..0dbc72a --- /dev/null +++ b/public/app/components/buttons/favorite-btn.component.js @@ -0,0 +1,47 @@ +class FavoriteBtnCtrl { + constructor(User, Articles, $state) { + 'ngInject'; + + this._Articles = Articles; + this._User = User; + this._$state = $state; + } + + submit() { + this.isSubmitting = true; + + if (!this._User.current) { + this._$state.go('app.register'); + return; + } + + if (this.article.favorited) { + this._Articles.unfavorite(this.article.slug).then( + () => { + this.isSubmitting = false; + this.article.favorited = false; + this.article.favoritesCount--; + } + ) + } else { + this._Articles.favorite(this.article.slug).then( + () => { + this.isSubmitting = false; + this.article.favorited = true; + this.article.favoritesCount++; + } + ) + } + } +} + +let FavoriteBtn = { + bindings: { + article: '=' + }, + transclude: true, + controller: FavoriteBtnCtrl, + templateUrl: 'components/buttons/favorite-btn.html' +}; + +export default FavoriteBtn; \ No newline at end of file diff --git a/public/app/components/buttons/favorite-btn.html b/public/app/components/buttons/favorite-btn.html new file mode 100644 index 0000000..9fd3f9f --- /dev/null +++ b/public/app/components/buttons/favorite-btn.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/public/app/components/buttons/follow-btn.component.js b/public/app/components/buttons/follow-btn.component.js new file mode 100644 index 0000000..61e69c4 --- /dev/null +++ b/public/app/components/buttons/follow-btn.component.js @@ -0,0 +1,45 @@ +class FollowBtnCtrl { + constructor(Profile, User, $state) { + 'ngInject'; + + this._Profile = Profile; + this._User = User; + + this._$state = $state; + } + + submit() { + this.isSubmitting = true; + + if (!this._User.current) { + this._$state.go('app.register'); + return; + } + + if (this.user.following) { + this._Profile.unfollow(this.user.username).then( + () => { + this.isSubmitting = false; + this.user.following = false; + } + ) + } else { + this._Profile.follow(this.user.username).then( + () => { + this.isSubmitting = false; + this.user.following = true; + } + ) + } + } +} + +let FollowBtn = { + bindings: { + user: '=' + }, + controller: FollowBtnCtrl, + templateUrl: 'components/buttons/follow-btn.html' +}; + +export default FollowBtn; \ No newline at end of file diff --git a/public/app/components/buttons/follow-btn.html b/public/app/components/buttons/follow-btn.html new file mode 100644 index 0000000..dc39ece --- /dev/null +++ b/public/app/components/buttons/follow-btn.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/public/app/components/index.js b/public/app/components/index.js new file mode 100644 index 0000000..4be029a --- /dev/null +++ b/public/app/components/index.js @@ -0,0 +1,29 @@ +import angular from 'angular'; + +let componentsModule = angular.module('app.components', []); + +import ListErrors from './list-errors.component'; +componentsModule.component('listErrors', ListErrors); + +import ShowAuthed from './show-authed.directive'; +componentsModule.directive('showAuthed', ShowAuthed); + +import FollowBtn from './buttons/follow-btn.component'; +componentsModule.component('followBtn', FollowBtn); + +import ArticleMeta from './article-helpers/article-meta.component'; +componentsModule.component('articleMeta', ArticleMeta); + +import FavoriteBtn from './buttons/favorite-btn.component'; +componentsModule.component('favoriteBtn', FavoriteBtn); + +import ArticlePreview from './article-helpers/article-preview.component'; +componentsModule.component('articlePreview', ArticlePreview); + +import ArticleList from './article-helpers/article-list.component'; +componentsModule.component('articleList', ArticleList); + +import ListPagination from './article-helpers/list-pagination.component'; +componentsModule.component('listPagination', ListPagination); + +export default componentsModule; diff --git a/public/app/components/list-errors.component.js b/public/app/components/list-errors.component.js new file mode 100644 index 0000000..2de72c9 --- /dev/null +++ b/public/app/components/list-errors.component.js @@ -0,0 +1,8 @@ +let ListErrors = { + bindings: { + errors: '=' + }, + templateUrl: 'components/list-errors.html' +}; + +export default ListErrors; \ No newline at end of file diff --git a/public/app/components/list-errors.html b/public/app/components/list-errors.html new file mode 100644 index 0000000..5858508 --- /dev/null +++ b/public/app/components/list-errors.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/public/app/components/show-authed.directive.js b/public/app/components/show-authed.directive.js new file mode 100644 index 0000000..fa18c1f --- /dev/null +++ b/public/app/components/show-authed.directive.js @@ -0,0 +1,28 @@ +function ShowAuthed(User) { + 'ngInject'; + + return { + restrict: 'A', + link: function(scope, element, attrs) { + scope.User = User; + + scope.$watch('User.current', function(val){ + if (val) { + if (attrs.showAuthed === 'true') { + element.css({ display: 'inherit' }) + } else { + element.css({ display: 'none' }) + } + } else { + if (attrs.showAuthed === 'true') { + element.css({ display: 'none' }) + } else { + element.css({ display: 'inherit' }) + } + } + }); + } + }; +} + +export default ShowAuthed; \ No newline at end of file diff --git a/public/app/config/app.config.js b/public/app/config/app.config.js new file mode 100644 index 0000000..d33d848 --- /dev/null +++ b/public/app/config/app.config.js @@ -0,0 +1,29 @@ +import authInterceptor from './auth.interceptor'; + +function AppConfig($httpProvider, $stateProvider, $locationProvider, $urlRouterProvider) { + 'ngInject'; + + $httpProvider.interceptors.push(authInterceptor); + + /* + If you don't want hashbang routing, uncomment this line. + Our tutorial will be using hashbang routing though :) + */ + // $locationProvider.html5Mode(true); + + $stateProvider + .state('app', { + abstract: true, + templateUrl: 'layout/app-view.html', + resolve: { + auth: function(User) { + return User.verifyAuth(); + } + } + }); + + $urlRouterProvider.otherwise('/'); + +} + +export default AppConfig; diff --git a/public/app/config/app.constants.js b/public/app/config/app.constants.js new file mode 100644 index 0000000..7ac5097 --- /dev/null +++ b/public/app/config/app.constants.js @@ -0,0 +1,8 @@ +const AppConstants = { + //api: 'http://localhost:3000/api', + api: 'https://conduit.productionready.io/api', + jwtKey: 'jwtToken', + appName: 'Conduit', +}; + +export default AppConstants; diff --git a/public/app/config/app.run.js b/public/app/config/app.run.js new file mode 100644 index 0000000..9e711f4 --- /dev/null +++ b/public/app/config/app.run.js @@ -0,0 +1,21 @@ +function AppRun(AppConstants, $rootScope) { + 'ngInject'; + + // change page title based on state + $rootScope.$on('$stateChangeSuccess', (event, toState) => { + $rootScope.setPageTitle(toState.title); + }); + + // Helper method for setting the page's title + $rootScope.setPageTitle = (title) => { + $rootScope.pageTitle = ''; + if (title) { + $rootScope.pageTitle += title; + $rootScope.pageTitle += ' \u2014 '; + } + $rootScope.pageTitle += AppConstants.appName; + }; + +} + +export default AppRun; diff --git a/public/app/config/app.templates.js b/public/app/config/app.templates.js new file mode 100644 index 0000000..e1bb4bb --- /dev/null +++ b/public/app/config/app.templates.js @@ -0,0 +1,2 @@ +angular.module('templates', []).run(['$templateCache', function($templateCache) {$templateCache.put('error.ejs','

<%= message %>

\n

<%= error.status %>

\n
<%= error.stack %>
\n'); +$templateCache.put('index.ejs','\r\n\r\n \r\n test\r\n\t\r\n \r\n \r\n\t\r\n \r\n \r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n
\r\n\t\r\n \r\n \r\n \r\n\t\r\n \r\n\t\r\n \r\n\r\n');}]); \ No newline at end of file diff --git a/public/app/config/auth.interceptor.js b/public/app/config/auth.interceptor.js new file mode 100644 index 0000000..478c893 --- /dev/null +++ b/public/app/config/auth.interceptor.js @@ -0,0 +1,21 @@ +function authInterceptor(JWT, AppConstants, $window, $q) { + 'ngInject'; + + return { + request: function(config) { + if (config.url.indexOf(AppConstants.api) === 0 && JWT.get()) { + config.headers.Authorization = 'Token ' + JWT.get(); + } + return config; + }, + responseError: function(rejection) { + if (rejection.status === 401) { + JWT.destroy(); + $window.location.reload(); + } + return $q.reject(rejection); + } + } +} + +export default authInterceptor; \ No newline at end of file diff --git a/public/app/services/articles.service.js b/public/app/services/articles.service.js new file mode 100644 index 0000000..d765c37 --- /dev/null +++ b/public/app/services/articles.service.js @@ -0,0 +1,75 @@ +export default class Articles { + constructor(AppConstants, $http, $q) { + 'ngInject'; + + this._AppConstants = AppConstants; + this._$http = $http; + this._$q = $q; + } + + save(article) { + let request = {}; + + if (article.slug) { + request.url = `${this._AppConstants.api}/articles/${article.slug}`; + request.method = 'PUT'; + delete article.slug; + } else { + request.url = `${this._AppConstants.api}/articles`; + request.method = 'POST'; + } + + request.data = { article: article }; + + return this._$http(request).then((res) => res.data.article); + } + + get(slug) { + let deferred = this._$q.defer(); + + if (!slug.replace(" ", "")) { + deferred.reject("Article slug is empty"); + return deferred.promise; + } + + this._$http({ + url: this._AppConstants.api + '/articles/' + slug, + method: 'GET' + }).then( + (res) => deferred.resolve(res.data.article), + (err) => deferred.reject(err) + ); + + return deferred.promise; + } + + destroy(slug) { + return this._$http({ + url: this._AppConstants.api + '/articles/' + slug, + method: 'DELETE' + }); + } + + favorite(slug) { + return this._$http({ + url: this._AppConstants.api + '/articles/' + slug + '/favorite', + method: 'POST' + }); + } + + unfavorite(slug) { + return this._$http({ + url: this._AppConstants.api + '/articles/' + slug + '/favorite', + method: 'DELETE' + }); + } + + query(config) { + let request = { + url: this._AppConstants.api + '/articles' + ((config.type === 'feed') ? '/feed' : ''), + method: 'GET', + params: config.filters ? config.filters : null + }; + return this._$http(request).then((res) => res.data); + } +} \ No newline at end of file diff --git a/public/app/services/comments.service.js b/public/app/services/comments.service.js new file mode 100644 index 0000000..5f9c310 --- /dev/null +++ b/public/app/services/comments.service.js @@ -0,0 +1,30 @@ +export default class Comments { + constructor(AppConstants, $http) { + 'ngInject'; + + this._AppConstants = AppConstants; + this._$http = $http; + } + + add(slug, payload) { + return this._$http({ + url: `${this._AppConstants.api}/articles/${slug}/comments`, + method: 'POST', + data: { comment: { body: payload } } + }).then((res) => res.data.comment); + } + + destroy(commentId, articleSlug) { + return this._$http({ + url: `${this._AppConstants.api}/articles/${articleSlug}/comments/${commentId}`, + method: 'DELETE' + }); + } + + getAll(slug) { + return this._$http({ + url: `${this._AppConstants.api}/articles/${slug}/comments`, + method: 'GET' + }).then((res) => res.data.comments); + } +} \ No newline at end of file diff --git a/public/app/services/index.js b/public/app/services/index.js new file mode 100644 index 0000000..f69d5c2 --- /dev/null +++ b/public/app/services/index.js @@ -0,0 +1,24 @@ +import angular from 'angular'; + +// Create the module where our functionality can attach to +let servicesModule = angular.module('app.services', []); + +import UserService from './user.service'; +servicesModule.service('User', UserService); + +import JwtService from './jwt.service'; +servicesModule.service('JWT', JwtService); + +import ProfileService from './profile.service'; +servicesModule.service('Profile', ProfileService); + +import ArticlesService from './articles.service'; +servicesModule.service('Articles', ArticlesService); + +import CommentsService from './comments.service'; +servicesModule.service('Comments', CommentsService); + +import TagsService from './tags.service'; +servicesModule.service('Tags', TagsService); + +export default servicesModule; diff --git a/public/app/services/jwt.service.js b/public/app/services/jwt.service.js new file mode 100644 index 0000000..a6ac405 --- /dev/null +++ b/public/app/services/jwt.service.js @@ -0,0 +1,20 @@ +export default class JWT { + constructor(AppConstants, $window) { + 'ngInject'; + + this._AppConstants = AppConstants; + this._$window = $window; + } + + save(token) { + this._$window.localStorage[this._AppConstants.jwtKey] = token; + } + + get() { + return this._$window.localStorage[this._AppConstants.jwtKey]; + } + + destroy() { + this._$window.localStorage.removeItem(this._AppConstants.jwtKey); + } +} \ No newline at end of file diff --git a/public/app/services/profile.service.js b/public/app/services/profile.service.js new file mode 100644 index 0000000..c11fcb9 --- /dev/null +++ b/public/app/services/profile.service.js @@ -0,0 +1,31 @@ +export default class Profile { + constructor(AppConstants, $http) { + 'ngInject'; + + this._AppConstants = AppConstants; + this._$http = $http; + } + + get(username) { + return this._$http({ + url: this._AppConstants.api + '/profiles/' + username, + method: 'GET' + }).then( + (res) => res.data.profile + ); + } + + follow(username) { + return this._$http({ + url: this._AppConstants.api + '/profiles/' + username + '/follow', + method: 'POST' + }).then((res) => res.data); + } + + unfollow(username) { + return this._$http({ + url: this._AppConstants.api + '/profiles/' + username + '/follow', + method: 'DELETE' + }).then((res) => res.data); + } +} \ No newline at end of file diff --git a/public/app/services/tags.service.js b/public/app/services/tags.service.js new file mode 100644 index 0000000..a912b5f --- /dev/null +++ b/public/app/services/tags.service.js @@ -0,0 +1,21 @@ +export default class Tags { + constructor(AppConstants, $http) { + 'ngInject'; + + this._AppConstants = AppConstants; + this._$http = $http; + + + } + + getAll() { + + return this._$http({ + url: this._AppConstants.api + '/tags', + method: 'GET', + }).then((res) => res.data.tags); + + } + + +} \ No newline at end of file diff --git a/public/app/services/user.service.js b/public/app/services/user.service.js new file mode 100644 index 0000000..8e5b68e --- /dev/null +++ b/public/app/services/user.service.js @@ -0,0 +1,92 @@ +export default class User { + constructor(JWT, AppConstants, $http, $state, $q) { + 'ngInject'; + + this._JWT = JWT; + this._AppConstants = AppConstants; + this._$http = $http; + this._$state = $state; + this._$q = $q; + + this.current = null; + } + + attemptAuth(type, credentials) { + let route = (type === 'login') ? '/login' : ''; + return this._$http({ + url: this._AppConstants.api + '/users' + route, + method: 'POST', + data: { + user: credentials + } + }).then( + (res) => { + this._JWT.save(res.data.user.token); + this.current = res.data.user; + + return res; + } + ); + } + + logout() { + this.current = null; + this._JWT.destroy(); + this._$state.go(this._$state.$current, null, { reload: true }); + } + + verifyAuth() { + let deferred = this._$q.defer(); + + if (!this._JWT.get()) { + deferred.resolve(false); + return deferred.promise; + } + + if (this.current) { + deferred.resolve(true); + } else { + this._$http({ + url: this._AppConstants.api + '/user', + method: 'GET', + }).then( + (res) => { + this.current = res.data.user; + deferred.resolve(true); + }, + (err) => { + this._JWT.destroy(); + deferred.resolve(false); + } + ); + } + return deferred.promise; + } + + ensureAuthIs(bool) { + let deferred = this._$q.defer(); + + this.verifyAuth().then((authValid) => { + if (authValid !== bool) { + this._$state.go('app.home'); + deferred.resolve(false); + } else { + deferred.resolve(true); + } + }) + return deferred.promise; + } + + update(fields) { + return this._$http({ + url: this._AppConstants.api + '/user', + method: 'PUT', + data: { user: fields } + }).then( + (res) => { + this.current = res.data.user; + return res.data.user; + } + ); + } +} \ No newline at end of file diff --git a/public/javascripts/main.js b/public/assets/javascripts/main.js similarity index 96% rename from public/javascripts/main.js rename to public/assets/javascripts/main.js index 7d30fb1..324e375 100644 --- a/public/javascripts/main.js +++ b/public/assets/javascripts/main.js @@ -13,8 +13,9 @@ $(window).load(function () { App.init(); for (var i in colors) { - $('#colorOptions').append(($('