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 @@
+
+ Contact
+