diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 7714498..075ba0e 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -67,6 +67,7 @@ class RegisterController extends Controller 'name' => $data['name'], 'email' => $data['email'], 'password' => bcrypt($data['password']), + 'api_token' => str_random(60) ]); } } diff --git a/app/User.php b/app/User.php index bfd96a6..7f08472 100644 --- a/app/User.php +++ b/app/User.php @@ -15,7 +15,7 @@ class User extends Authenticatable * @var array */ protected $fillable = [ - 'name', 'email', 'password', + 'name', 'email', 'password', 'api_token' ]; /** @@ -24,6 +24,6 @@ class User extends Authenticatable * @var array */ protected $hidden = [ - 'password', 'remember_token', + 'password', 'remember_token', 'api_token' ]; } diff --git a/database/migrations/2017_11_21_193450_add_api_token_to_users_table.php b/database/migrations/2017_11_21_193450_add_api_token_to_users_table.php new file mode 100644 index 0000000..7655086 --- /dev/null +++ b/database/migrations/2017_11_21_193450_add_api_token_to_users_table.php @@ -0,0 +1,32 @@ +string('api_token', 60)->unique(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function(Blueprint $table) { + $table->dropColumn('api_token'); + }); + } +} diff --git a/gulpfile.js b/gulpfile.js index faf966c..000d4a2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,25 +1,39 @@ -// include packages +// Core packages const gulp = require("gulp"), gutil = require("gulp-util"), plumber = require("gulp-plumber"), - concat = require("gulp-concat"), - sass = require("gulp-sass"), + concat = require("gulp-concat"); + +// Sass packages +const sass = require("gulp-sass"), sassGlob = require("gulp-sass-glob"), postCSS = require("gulp-postcss"), - autoprefixer = require("autoprefixer"), - babel = require("gulp-babel"), + autoprefixer = require("autoprefixer"); + +// Javascript packages +const babel = require("gulp-babel"), stripDebug = require("gulp-strip-debug"), uglify = require("gulp-uglify"); -// determine if gulp has been run with --production -const prod = gutil.env.production; +// Vue packages +const browserify = require("browserify"), + vueify = require("vueify"), + source = require("vinyl-source-stream"), + buffer = require("vinyl-buffer"); -// declare plugin settings -const sassOutputStyle = prod ? "compressed" : "nested", - sassIncludePaths = [ "bower_components" ], - autoprefixerSettings = { remove: false, cascade: false, browsers: [ "last 6 versions" ] }; +// Determine if gulp has been run with --production +const isProduction = gutil.env.production; -// javascript files for the public site +// Declare plugin settings +const sassOutputStyle = isProduction ? "compressed" : "nested", + sassPaths = [ "bower_components", "node_modules" ], + autoprefixerSettings = { remove: false, cascade: false, browsers: [ "last 6 versions" ] }, + vuePaths = [ "./bower_components", "./node_modules", "./resources/components", "./resources/assets/js" ]; + +// Vue file for the public site +const vuePublic = "resources/assets/js/app-vue.js"; + +// Javascript files for the public site const jsPublic = [ "resources/assets/js/site-vars.js", "resources/assets/js/contact.js", @@ -27,7 +41,7 @@ const jsPublic = [ "resources/assets/js/app.js" ]; -// javascript libraries for the public site +// Javascript libraries for the public site const jsPublicLibs = [ "bower_components/jquery/dist/jquery.js", "bower_components/bootstrap-sass/assets/javascripts/bootstrap.js", @@ -35,12 +49,12 @@ const jsPublicLibs = [ "node_modules/what-input/dist/what-input.js" ]; -// javascript files for the dashboard +// Javascript files for the dashboard const jsDashboard = [ "resources/assets/js/dashboard.js" ]; -// javascript libraries for the dashboard +// Javascript libraries for the dashboard const jsDashboardLibs = [ "bower_components/jquery/dist/jquery.js", "bower_components/bootstrap-sass/assets/javascripts/bootstrap.js", @@ -50,102 +64,128 @@ const jsDashboardLibs = [ "bower_components/simplemde/dist/simplemde.min.js" ]; -// paths to folders containing fonts that should be copied to public/fonts/ +// Paths to folders containing fonts that should be copied to public/fonts/ const fontPaths = [ "resources/assets/fonts/**", "bower_components/bootstrap-sass/assets/fonts/**/*", "bower_components/fontawesome/fonts/**" ]; -// function to handle gulp-plumber errors -function plumberError(err) { - console.log(err); +// Handle errors +function handleError(err) { + gutil.log(err); this.emit("end"); } -// function to handle the processing of sass files +// Process sass function processSass(filename) { return gulp.src("resources/assets/sass/" + filename + ".scss") - .pipe(plumber(plumberError)) + .pipe(plumber(handleError)) .pipe(sassGlob()) - .pipe(sass({ outputStyle: sassOutputStyle, includePaths: sassIncludePaths })) + .pipe(sass({ outputStyle: sassOutputStyle, includePaths: sassPaths })) .pipe(postCSS([ autoprefixer(autoprefixerSettings) ])) .pipe(concat(filename + ".css")) .pipe(gulp.dest("public/css/")); } -// function to handle the processing of javascript files -function processJavaScript(ouputFilename, inputFiles, es6) { - const javascript = gulp.src(inputFiles) - .pipe(plumber(plumberError)) - .pipe(concat(ouputFilename + ".js")); +// Process vue +function processVue(ouputFilename, inputFile) { + const javascript = browserify({ + entries: [ inputFile ], + paths: vuePaths + }).transform("babelify") + .transform(vueify) + .bundle() + .on("error", handleError) + .pipe(source(ouputFilename + ".js")) + .pipe(buffer()); - if (es6) { javascript.pipe(babel()); } - if (prod) { javascript.pipe(stripDebug()).pipe(uglify()); } + if (isProduction) { javascript.pipe(stripDebug()).pipe(uglify().on("error", handleError)); } return javascript.pipe(gulp.dest("public/js/")); } -// gulp task for public styles +// Process javascript +function processJavaScript(ouputFilename, inputFiles, es6) { + const javascript = gulp.src(inputFiles) + .pipe(plumber(handleError)) + .pipe(concat(ouputFilename + ".js")); + + if (es6) { javascript.pipe(babel()); } + if (isProduction) { javascript.pipe(stripDebug()).pipe(uglify()); } + return javascript.pipe(gulp.dest("public/js/")); +} + +// Task for public styles gulp.task("sass-public", function() { return processSass("app"); }); -// gulp task for dashboard styles +// Task for dashboard styles gulp.task("sass-dashboard", function() { return processSass("dashboard"); }); -// gulp task for public javascript +// Task for public vue +gulp.task("js-public-vue", function() { + return processVue("app-vue", vuePublic); +}); + +// Task for public javascript gulp.task("js-public", function() { return processJavaScript("app", jsPublic, true); }); -// gulp task for public javascript libraries +// Task for public javascript libraries gulp.task("js-public-libs", function() { return processJavaScript("lib", jsPublicLibs, false); }); -// gulp task for dashboard javascript +// Task for dashboard javascript gulp.task("js-dashboard", function() { return processJavaScript("dashboard", jsDashboard, true); }); -// gulp task for dashboard javascript libraries +// Task for dashboard javascript libraries gulp.task("js-dashboard-libs", function() { return processJavaScript("lib-dashboard", jsDashboardLibs, false); }); -// gulp task to copy fonts +// Task to copy fonts gulp.task("fonts", function() { return gulp.src(fontPaths) - .pipe(plumber(plumberError)) + .pipe(plumber(handleError)) .pipe(gulp.dest("public/fonts/")); }); -// gulp watch task +// Task to run tasks when their respective files are changed gulp.task("watch", function() { - const gLiveReload = require("gulp-livereload"); + const livereload = require("gulp-livereload"); const liveReloadUpdate = function(files, wait) { setTimeout(function() { - gLiveReload.changed(files); + livereload.changed(files); }, wait || 1); }; - gLiveReload.listen(); + livereload.listen(); gulp.watch(jsPublic, [ "js-public" ]).on("change", liveReloadUpdate); gulp.watch(jsDashboard, [ "js-dashboard" ]).on("change", liveReloadUpdate); gulp.watch([ "app/**/*.php", "routes/**/*.php", "resources/views/**/*.blade.php" ]).on("change", liveReloadUpdate); + gulp.watch([ vuePublic, "resources/assets/js/mixins/**/*.js", "resources/components/**/*.vue" ], [ "js-public-vue" ]).on("change", function(files) { + liveReloadUpdate(files, 3000); + }); + gulp.watch("resources/assets/sass/**/*.scss", [ "sass-public", "sass-dashboard" ]).on("change", function(files) { liveReloadUpdate(files, 1000); }); }); -// gulp default task +// Task to run non-development tasks gulp.task("default", [ "sass-public", "sass-dashboard", + "js-public-vue", "js-public", "js-public-libs", "js-dashboard", diff --git a/package.json b/package.json index ff660a0..842697d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "dependencies": { "autoprefixer": "^7.1.6", "babel-core": "^6.26.0", + "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-env": "^1.6.1", + "babelify": "^8.0.0", + "browserify": "^14.5.0", + "es6-promise": "^4.1.1", "gsap": "^1.20.3", "gulp": "^3.9.1", "gulp-babel": "^7.0.0", @@ -22,6 +26,15 @@ "gulp-strip-debug": "^1.1.0", "gulp-uglify": "^3.0.0", "gulp-util": "^3.0.8", + "vinyl-buffer": "^1.0.0", + "vinyl-source-stream": "^1.1.0", + "vue": "^2.5.8", + "vue-resource": "^1.3.4", + "vue-router": "^3.0.1", + "vue-template-compiler": "^2.5.8", + "vueify": "^9.4.1", + "vuex": "^3.0.1", + "vuex-router-sync": "^5.0.0", "what-input": "^5.0.3" } } diff --git a/resources/assets/js/app-vue.js b/resources/assets/js/app-vue.js new file mode 100644 index 0000000..95f6a5e --- /dev/null +++ b/resources/assets/js/app-vue.js @@ -0,0 +1,120 @@ +// Determine whether to use vue.js in debug or production mode +const Vue = env.debug ? require("vue/dist/vue.js") : require("vue/dist/vue.min.js"); + +// Import plugins +import VueRouter from "vue-router"; +import VueResource from "vue-resource"; +import Vuex from "vuex"; +import { sync } from "vuex-router-sync"; + +// Load plugins +Vue.use(VueRouter); +Vue.use(VueResource); +Vue.use(Vuex); + +// CSRF prevention header +Vue.http.headers.common["X-CSRF-TOKEN"] = env.csrfToken; + +// Import page components +import HomePage from "pages/home.vue"; +import ContactPage from "pages/contact.vue"; +import Error404Page from "pages/error404.vue"; + +// Import section components +import NavSection from "sections/nav.vue"; +import FooterSection from "sections/footer.vue"; + +// Name the nav and footer components so they can be used globally +Vue.component("nav-component", NavSection); +Vue.component("footer-component", FooterSection); + +// Create a router instance +const router = new VueRouter({ + mode: "history", + linkActiveClass: "active", + root: "/", + routes: [ + { path: "/", component: HomePage }, + { path: "/contact", component: ContactPage }, + { path: "/*", component: Error404Page } + ] +}); + +// Create a vuex store instance +const store = new Vuex.Store({ + state: { + appName: env.appName, + firstLoad: true, + lastPath: "" + }, + + getters: { + getAppName: state => { + return state.appName; + }, + + getFirstLoad: state => { + return state.firstLoad; + }, + + getLastPath: state => { + return state.lastPath; + } + }, + + mutations: { + setFirstLoad(state, value) { + state.firstLoad = value; + }, + + setLastPath(state, value) { + state.lastPath = value; + } + }, + + actions: { + + } +}); + +// Sync vue-router-sync with vuex store +sync(store, router); + +// Functionality to run before page load and change +router.beforeEach((to, from, next) => { + if (to.path !== store.getters.getLastPath) { + if (store.getters.getFirstLoad) { + next(); + } else { + // Fade the page out and scroll when moving from one page to another + TweenMax.to("#router-view", 0.25, { + opacity: 0, + onComplete: () => { + $("html, body").scrollTop(0); + next(); + } + }); + } + } +}); + +// Functionality to run on page load and change +router.afterEach((to, from) => { + if (to.path !== store.getters.getLastPath) { + store.commit("setLastPath", to.path); + + if (store.getters.getFirstLoad) { + // Set Page.firstLoad to false so we know the initial load has completed + store.commit("setFirstLoad", false); + } else { + Vue.nextTick(() => { + TweenMax.to("#router-view", 0.25, { opacity: 1 }); + }); + } + } +}); + +const App = new Vue({ + router, + store +}).$mount("#page-content"); diff --git a/resources/assets/js/mixins/base-page.js b/resources/assets/js/mixins/base-page.js new file mode 100644 index 0000000..80d2e69 --- /dev/null +++ b/resources/assets/js/mixins/base-page.js @@ -0,0 +1,78 @@ +export default { + data() { + return { + metaTitle: "", + metaDescription: "", + metaKeywords: "", + + metaTags: { + "title": [ "name", "title" ], + "description": [ "name", "description" ], + "keywords": [ "name", "keywords" ], + "dc:title": [ "name", "title" ], + "dc:description": [ "name", "description" ], + "og:title": [ "property", "title" ], + "og:description": [ "property", "description" ], + "og:url": [ "property", "url" ], + "twitter:title": [ "name", "title" ], + "twitter:description": [ "name", "description" ] + } + }; + }, + + computed: { + pageTitle() { + return this.metaTitle === "" ? env.appName : `${this.metaTitle} | ${env.appName}`; + }, + + pageDescription() { + return this.metaDescription === "" ? env.appDesc : this.metaDescription; + }, + + fullPath() { + return document.location.origin + this.$route.path; + } + }, + + methods: { + updateMetaTag(name, attribute, content) { + const $tag = $("meta[" + name + "=" + attribute.replace(/:/, "\\:") + "]"); + + if ($tag.length) { + $tag.attr("content", content); + } + }, + + updateMetaData() { + let metaContent; + + document.title = this.pageTitle; + $("link[rel=canonical]").attr("href", this.fullPath); + + Object.keys(this.metaTags).forEach((name) => { + switch (this.metaTags[name][1]) { + case "title": + metaContent = this.pageTitle; + break; + case "description": + metaContent = this.pageDescription; + break; + case "keywords": + metaContent = this.metaKeywords; + break; + case "url": + metaContent = this.fullPath; + break; + default: + metaContent = ""; + } + + this.updateMetaTag(this.metaTags[name][0], name, metaContent); + }); + } + }, + + created() { + this.updateMetaData(); + } +}; diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index ab811a2..3675fc8 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -6,7 +6,7 @@ @import "bootstrap-sass/assets/stylesheets/_bootstrap.scss"; // Supplementary -@import "elements/**/*.scss"; +@import "sections/**/*.scss"; @import "pages/**/*.scss"; // @@ -38,13 +38,13 @@ body { @media (max-width: $grid-float-breakpoint-max) { padding-top: $nav-height-mobile; } } -#page-container { +.page-container { display: flex; min-height: 100vh; padding-top: $nav-height; flex-direction: column; - #main-content { + .main-content { flex-grow: 1; } } diff --git a/resources/assets/sass/pages/_contact.scss b/resources/assets/sass/pages/_contact.scss index d2f97b3..47635a7 100644 --- a/resources/assets/sass/pages/_contact.scss +++ b/resources/assets/sass/pages/_contact.scss @@ -1,53 +1,51 @@ -.page-contact { - #contact-form { - $trans-speed: 100ms; - margin-top: 35px; +.contact-page-component { + $trans-speed: 100ms; + margin-top: 35px; + margin-bottom: 20px; + + input, textarea { margin-bottom: 20px; + width: 100%; + padding: 5px 10px; + border: 2px solid fade-out($c-text, 0.75); + background-color: rgba(255, 255, 255, 0.8); + font-size: 14px; + transition: border $trans-speed; + &:focus { border: 2px solid fade-out($c-base, 0.4); } + &.error { border: 2px solid $c-error; } + } - input, textarea { - margin-bottom: 20px; - width: 100%; - padding: 5px 10px; - border: 2px solid fade-out($c-text, 0.75); - background-color: rgba(255, 255, 255, 0.8); - font-size: 14px; - transition: border $trans-speed; - &:focus { border: 2px solid fade-out($c-base, 0.4); } - &.error { border: 2px solid $c-error; } - } + textarea { + resize: none; + height: 150px; + } - textarea { - resize: none; - height: 150px; - } + .submit { + background-color: lighten($c-base, 5%); + color: $c-text-light; + font-weight: bold; + text-align: center; + transition: background-color $trans-speed; + &:hover { background-color: $c-base; } + &.disabled { background-color: $c-base; } + } - .submit { - background-color: lighten($c-base, 5%); - color: $c-text-light; + .notification { + margin: 0px auto 15px auto; + padding: 5px 10px; + background-color: lighten($c-error, 15%); + color: $c-text-light; + font-size: 14px; + text-align: center; + opacity: 0; + transition: opacity $trans-speed; + span { font-weight: bold; } + &.visible { opacity: 1; } + + &.success { + background-color: transparent; + color: $c-text; font-weight: bold; - text-align: center; - transition: background-color $trans-speed; - &:hover { background-color: $c-base; } - &.disabled { background-color: $c-base; } - } - - .notification { - margin: 0px auto 15px auto; - padding: 5px 10px; - background-color: lighten($c-error, 15%); - color: $c-text-light; - font-size: 14px; - text-align: center; - opacity: 0; - transition: opacity $trans-speed; - span { font-weight: bold; } - &.visible { opacity: 1; } - - &.success { - background-color: transparent; - color: $c-text; - font-weight: bold; - } } } } diff --git a/resources/assets/sass/elements/_footer.scss b/resources/assets/sass/sections/_footer.scss similarity index 56% rename from resources/assets/sass/elements/_footer.scss rename to resources/assets/sass/sections/_footer.scss index 9802937..cb87d3c 100644 --- a/resources/assets/sass/elements/_footer.scss +++ b/resources/assets/sass/sections/_footer.scss @@ -1,14 +1,8 @@ -footer { +.footer-section-component { width: 100%; height: 32px; padding-left: 10px; background-color: $c-base; color: $c-text-light; line-height: 32px; - - &.sticky-footer { - position: absolute; - bottom: 0px; - width: 100%; - } } diff --git a/resources/assets/sass/elements/_nav.scss b/resources/assets/sass/sections/_nav.scss similarity index 98% rename from resources/assets/sass/elements/_nav.scss rename to resources/assets/sass/sections/_nav.scss index 2179d0a..21f87ac 100644 --- a/resources/assets/sass/elements/_nav.scss +++ b/resources/assets/sass/sections/_nav.scss @@ -1,4 +1,4 @@ -.navbar { +.nav-section-component { z-index: 1; margin-bottom: 0px; height: $nav-height; diff --git a/resources/assets/sass/elements/subscription-form.scss b/resources/assets/sass/sections/_subscription-form.scss similarity index 92% rename from resources/assets/sass/elements/subscription-form.scss rename to resources/assets/sass/sections/_subscription-form.scss index 230deae..ebe4137 100644 --- a/resources/assets/sass/elements/subscription-form.scss +++ b/resources/assets/sass/sections/_subscription-form.scss @@ -1,4 +1,4 @@ -#subscription-form { +.subscription-form-section-component { $trans-speed: 100ms; position: absolute; top: 50%; diff --git a/resources/components/pages/contact.vue b/resources/components/pages/contact.vue new file mode 100644 index 0000000..b054b78 --- /dev/null +++ b/resources/components/pages/contact.vue @@ -0,0 +1,100 @@ + + + diff --git a/resources/components/pages/error404.vue b/resources/components/pages/error404.vue new file mode 100644 index 0000000..39b7dce --- /dev/null +++ b/resources/components/pages/error404.vue @@ -0,0 +1,22 @@ + + + diff --git a/resources/components/pages/home.vue b/resources/components/pages/home.vue new file mode 100644 index 0000000..3306de4 --- /dev/null +++ b/resources/components/pages/home.vue @@ -0,0 +1,26 @@ + + + diff --git a/resources/components/sections/footer.vue b/resources/components/sections/footer.vue new file mode 100644 index 0000000..4844011 --- /dev/null +++ b/resources/components/sections/footer.vue @@ -0,0 +1,5 @@ + diff --git a/resources/components/sections/nav.vue b/resources/components/sections/nav.vue new file mode 100644 index 0000000..00c9681 --- /dev/null +++ b/resources/components/sections/nav.vue @@ -0,0 +1,23 @@ + diff --git a/resources/components/sections/subscription-form.vue b/resources/components/sections/subscription-form.vue new file mode 100644 index 0000000..b1f565e --- /dev/null +++ b/resources/components/sections/subscription-form.vue @@ -0,0 +1,62 @@ + + + diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 8ccfae1..302011b 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -1,6 +1,6 @@ -@extends('layouts.dashboard') +@extends('templates.dashboard') -@section('content') +@section('page-content')
diff --git a/resources/views/auth/passwords/email.blade.php b/resources/views/auth/passwords/email.blade.php index 98df540..dc06385 100644 --- a/resources/views/auth/passwords/email.blade.php +++ b/resources/views/auth/passwords/email.blade.php @@ -1,6 +1,6 @@ -@extends('layouts.dashboard') +@extends('templates.dashboard') -@section('content') +@section('page-content')
diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php index 3ed9731..7e17148 100644 --- a/resources/views/auth/passwords/reset.blade.php +++ b/resources/views/auth/passwords/reset.blade.php @@ -1,6 +1,6 @@ -@extends('layouts.dashboard') +@extends('templates.dashboard') -@section('content') +@section('page-content')
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 096dd62..020968a 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -1,6 +1,6 @@ -@extends('layouts.dashboard') +@extends('templates.dashboard') -@section('content') +@section('page-content')
diff --git a/resources/views/dashboard/core.blade.php b/resources/views/dashboard/core.blade.php index b3c6966..bb93a30 100644 --- a/resources/views/dashboard/core.blade.php +++ b/resources/views/dashboard/core.blade.php @@ -1,6 +1,6 @@ -@extends('layouts.dashboard') +@extends('templates.dashboard') -@section('content') +@section('page-content')
diff --git a/resources/views/dashboard/home.blade.php b/resources/views/dashboard/home.blade.php index a4ba775..5ab2e95 100644 --- a/resources/views/dashboard/home.blade.php +++ b/resources/views/dashboard/home.blade.php @@ -2,5 +2,5 @@ @section('dashboard-body') @set('menu_class', 'list-group-item') -
    @include('dashboard.elements.menu')
+
    @include('dashboard.sections.menu')
@endsection diff --git a/resources/views/dashboard/elements/menu.blade.php b/resources/views/dashboard/sections/menu.blade.php similarity index 100% rename from resources/views/dashboard/elements/menu.blade.php rename to resources/views/dashboard/sections/menu.blade.php diff --git a/resources/views/dashboard/elements/nav.blade.php b/resources/views/dashboard/sections/nav.blade.php similarity index 96% rename from resources/views/dashboard/elements/nav.blade.php rename to resources/views/dashboard/sections/nav.blade.php index df3ea44..178143f 100644 --- a/resources/views/dashboard/elements/nav.blade.php +++ b/resources/views/dashboard/sections/nav.blade.php @@ -23,7 +23,7 @@ @endif @else @set('menu_class', 'nav-item') - @include('dashboard.elements.menu') + @include('dashboard.sections.menu')