허용운 5 years ago
commit
c8229e352c
86 changed files with 16078 additions and 0 deletions
  1. 90 0
      .gitignore
  2. 22 0
      README.md
  3. 7 0
      assets/README.md
  4. 7 0
      assets/scss/_bulma.scss
  5. 96 0
      assets/scss/_instructor_header.scss
  6. 3 0
      assets/scss/main.scss
  7. 80 0
      assets/scss/spacing.scss
  8. 38 0
      components/BlogCard.vue
  9. 59 0
      components/CourseCard.vue
  10. 79 0
      components/Logo.vue
  11. 7 0
      components/README.md
  12. 83 0
      components/form/MultiLineTextInput.vue
  13. 55 0
      components/form/TextInputWithCount.vue
  14. 59 0
      components/instructor/CourseCreateStep1.vue
  15. 71 0
      components/instructor/CourseCreateStep2.vue
  16. 85 0
      components/instructor/LandingPage.vue
  17. 27 0
      components/instructor/Price.vue
  18. 23 0
      components/instructor/Status.vue
  19. 51 0
      components/instructor/TargetStudents.vue
  20. 45 0
      components/shared/ErrorView.vue
  21. 59 0
      components/shared/Header.vue
  22. 62 0
      components/shared/Hero.vue
  23. 117 0
      components/shared/Navbar.vue
  24. 12 0
      helpers/validators.js
  25. 7 0
      layouts/README.md
  26. 65 0
      layouts/default.vue
  27. 71 0
      layouts/instructor.vue
  28. 8 0
      middleware/README.md
  29. 9 0
      middleware/admin.js
  30. 10 0
      middleware/auth.js
  31. 7 0
      middleware/guest.js
  32. 39 0
      mixins/MultiComponentMixin.js
  33. 62 0
      nuxt.config.js
  34. 12105 0
      package-lock.json
  35. 44 0
      package.json
  36. 6 0
      pages/README.md
  37. 120 0
      pages/index.vue
  38. 163 0
      pages/instructor/course/_id/manage.vue
  39. 159 0
      pages/instructor/course/create.vue
  40. 3 0
      pages/instructor/course/index.vue
  41. 155 0
      pages/instructor/courses/index.vue
  42. 67 0
      pages/instructor/index.vue
  43. 139 0
      pages/login.vue
  44. 20 0
      pages/notAuthenticated.vue
  45. 20 0
      pages/notAuthorized.vue
  46. 212 0
      pages/register.vue
  47. 14 0
      pages/secret.vue
  48. 7 0
      plugins/README.md
  49. 9 0
      plugins/filters.js
  50. 4 0
      plugins/toasted.js
  51. 4 0
      plugins/vuelidate.js
  52. 18 0
      server/controllers/api.js
  53. 20 0
      server/controllers/auth.js
  54. 173 0
      server/controllers/blog.js
  55. 13 0
      server/controllers/category.js
  56. 58 0
      server/controllers/product-hero.js
  57. 112 0
      server/controllers/product.js
  58. 100 0
      server/controllers/user.js
  59. 29 0
      server/db/index.js
  60. 112 0
      server/dbMocks/data.js
  61. 47 0
      server/dbMocks/seedDb.js
  62. 33 0
      server/index.js
  63. 5 0
      server/keys/index.js
  64. 4 0
      server/keys/prod.js
  65. 20 0
      server/models/blog.js
  66. 9 0
      server/models/category.js
  67. 14 0
      server/models/product-hero.js
  68. 29 0
      server/models/product.js
  69. 61 0
      server/models/user.js
  70. 8 0
      server/routes/api.js
  71. 32 0
      server/routes/blog.js
  72. 8 0
      server/routes/category.js
  73. 63 0
      server/routes/index.js
  74. 17 0
      server/routes/product-hero.js
  75. 25 0
      server/routes/product.js
  76. 8 0
      server/routes/test.js
  77. 13 0
      server/routes/user.js
  78. 45 0
      server/services/passport.js
  79. 11 0
      static/README.md
  80. BIN
      static/favicon.ico
  81. 10 0
      store/README.md
  82. 91 0
      store/auth.js
  83. 34 0
      store/category.js
  84. 14 0
      store/course.js
  85. 14 0
      store/index.js
  86. 62 0
      store/instructor/course.js

+ 90 - 0
.gitignore

@@ -0,0 +1,90 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Node template
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# 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 (https://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
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# next.js build output
+.next
+
+# nuxt.js build output
+.nuxt
+
+# Nuxt generate
+dist
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless
+
+# IDE / Editor
+.idea
+.editorconfig
+
+# Service worker
+sw.*
+
+# Mac OSX
+.DS_Store
+
+server/keys/dev.js

+ 22 - 0
README.md

@@ -0,0 +1,22 @@
+# nuxt-promo-template
+
+> My super Nuxt.js project
+
+## Build Setup
+
+``` bash
+# install dependencies
+$ npm run install
+
+# serve with hot reload at localhost:3000
+$ npm run dev
+
+# build for production and launch server
+$ npm run build
+$ npm run start
+
+# generate static project
+$ npm run generate
+```
+
+For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org).

+ 7 - 0
assets/README.md

@@ -0,0 +1,7 @@
+# ASSETS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).

+ 7 - 0
assets/scss/_bulma.scss

@@ -0,0 +1,7 @@
+$primary: #4bacff;
+$danger: #dc5222;
+
+$navbar-height: 5rem;
+$navbar-item-img-max-height: 3rem;
+
+@import "~bulma"

+ 96 - 0
assets/scss/_instructor_header.scss

@@ -0,0 +1,96 @@
+.full-page-takeover {
+  &-page {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+  }
+
+  &-window {
+    position: fixed;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: #fff;
+    z-index: 1050;
+  }
+
+  &-container {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    overflow-y: auto;
+  }
+}
+
+.full-page-takeover-header {
+    display: flex;
+    height: 60px;
+    align-items: center;
+    padding: 40px 0;
+
+    &-bottom {
+      &-progress {
+        background: #e8e9eb;
+        height: 6px;
+        overflow-x: hidden;
+
+        &-highlight {
+          // width: 50%;
+          background: #a1a7b3;
+          box-sizing: content-box;
+          border-radius: 0 3px 3px 0;
+          height: 100%;
+          padding-right: 3px;
+          transition: .6s ease-out;
+        }
+      }
+    }
+
+    &-logo {
+      padding: 0 20px;
+
+      &-title {
+        font-size: 35px;
+      }
+    }
+
+    &-divider {
+      align-self: stretch;
+      border-right: 2px solid #dedfe0;
+    }
+
+    &-text {
+      color: #686f7a;
+      flex: 1 1;
+      padding: 0 30px;
+      font-size: 18px;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+    }
+
+    &-button {
+      padding-right: 17px;
+      margin-left: auto;
+
+      > a {
+        padding: 11px 12px;
+        font-size: 21px;
+      }
+    }
+  }
+
+.full-page-footer {
+  &-row {
+    padding: 15px 0;
+    border-top: 1px solid #dedfe0;
+  }
+
+  &-col {
+    width: 50%;
+    float: left;
+    padding-left: 15px;
+    padding-right: 15px;
+  }
+}

+ 3 - 0
assets/scss/main.scss

@@ -0,0 +1,3 @@
+@import "./bulma";
+@import "./spacing";
+@import "./instructor_header";

+ 80 - 0
assets/scss/spacing.scss

@@ -0,0 +1,80 @@
+.m-none { margin: 0 !important; }
+.p-none { padding: 0 !important; }
+.m-t-none { margin-top: 0 !important; }
+.p-t-none { padding-top: 0 !important; }
+.m-r-none { margin-right: 0 !important; }
+.p-r-none { padding-right: 0 !important; }
+.m-b-none { margin-bottom: 0 !important; }
+.p-b-none { padding-bottom: 0 !important; }
+.m-l-none { margin-left: 0 !important; }
+.p-l-none { padding-left: 0 !important; }
+.m-xxs { margin: 0.125rem !important; }
+.p-xxs { padding: 0.125rem !important; }
+.m-t-xxs { margin-top: 0.125rem !important; }
+.p-t-xxs { padding-top: 0.125rem !important; }
+.m-r-xxs { margin-right: 0.125rem !important; }
+.p-r-xxs { padding-right: 0.125rem !important; }
+.m-b-xxs { margin-bottom: 0.125rem !important; }
+.p-b-xxs { padding-bottom: 0.125rem !important; }
+.m-l-xxs { margin-left: 0.125rem !important; }
+.p-l-xxs { padding-left: 0.125rem !important; }
+.m-xs { margin: 0.25rem !important; }
+.p-xs { padding: 0.25rem !important; }
+.m-t-xs { margin-top: 0.25rem !important; }
+.p-t-xs { padding-top: 0.25rem !important; }
+.m-r-xs { margin-right: 0.25rem !important; }
+.p-r-xs { padding-right: 0.25rem !important; }
+.m-b-xs { margin-bottom: 0.25rem !important; }
+.p-b-xs { padding-bottom: 0.25rem !important; }
+.m-l-xs { margin-left: 0.25rem !important; }
+.p-l-xs { padding-left: 0.25rem !important; }
+.m-sm { margin: 0.5rem !important; }
+.p-sm { padding: 0.5rem !important; }
+.m-t-sm { margin-top: 0.5rem !important; }
+.p-t-sm { padding-top: 0.5rem !important; }
+.m-r-sm { margin-right: 0.5rem !important; }
+.p-r-sm { padding-right: 0.5rem !important; }
+.m-b-sm { margin-bottom: 0.5rem !important; }
+.p-b-sm { padding-bottom: 0.5rem !important; }
+.m-l-sm { margin-left: 0.5rem !important; }
+.p-l-sm { padding-left: 0.5rem !important; }
+.m-md { margin: 1rem !important; }
+.p-md { padding: 1rem !important; }
+.m-t-md { margin-top: 1rem !important; }
+.p-t-md { padding-top: 1rem !important; }
+.m-r-md { margin-right: 1rem !important; }
+.p-r-md { padding-right: 1rem !important; }
+.m-b-md { margin-bottom: 1rem !important; }
+.p-b-md { padding-bottom: 1rem !important; }
+.m-l-md { margin-left: 1rem !important; }
+.p-l-md { padding-left: 1rem !important; }
+.m-lg { margin: 2rem !important; }
+.p-lg { padding: 2rem !important; }
+.m-t-lg { margin-top: 2rem !important; }
+.p-t-lg { padding-top: 2rem !important; }
+.m-r-lg { margin-right: 2rem !important; }
+.p-r-lg { padding-right: 2rem !important; }
+.m-b-lg { margin-bottom: 2rem; }
+.p-b-lg { padding-bottom: 2rem !important; }
+.m-l-lg { margin-left: 2rem !important; }
+.p-l-lg { padding-left: 2rem !important; }
+.m-xl { margin: 4rem !important; }
+.p-xl { padding: 4rem !important; }
+.m-t-xl { margin-top: 4rem !important; }
+.p-t-xl { padding-top: 4rem !important; }
+.m-r-xl { margin-right: 4rem !important; }
+.p-r-xl { padding-right: 4rem !important; }
+.m-b-xl { margin-bottom: 4rem; }
+.p-b-xl { padding-bottom: 4rem !important; }
+.m-l-xl { margin-left: 4rem !important; }
+.p-l-xl { padding-left: 4rem !important; }
+.m-xxl { margin: 8rem !important; }
+.p-xxl { padding: 8rem !important; }
+.m-t-xxl { margin-top: 8rem !important; }
+.p-t-xxl { padding-top: 8rem !important; }
+.m-r-xxl { margin-right: 8rem !important; }
+.p-r-xxl { padding-right: 8rem !important; }
+.m-b-xxl { margin-bottom: 8rem !important; }
+.p-b-xxl { padding-bottom: 8rem !important; }
+.m-l-xxl { margin-left: 8rem !important; }
+.p-l-xxl { padding-left: 8rem !important; }

+ 38 - 0
components/BlogCard.vue

@@ -0,0 +1,38 @@
+<template>
+<div class="card">
+  <div class="card-content">
+    <div class="media">
+      <div class="media-content">
+        <p class="title is-4">Some Super Title</p>
+        <p class="subtitle is-6"><i>Some Super Subtitle</i></p>
+      </div>
+    </div>
+    <div class="content">
+      Some Description
+      <br>
+    </div>
+  </div>
+  <footer class="card-footer">
+    <nuxt-link :to="'#'" class="card-footer-item">Read More</nuxt-link>
+  </footer>
+</div>
+</template>
+
+<style lang="scss" scoped>
+  .card-image:hover {
+    cursor: pointer;
+    opacity: 0.9;
+  }
+  .price-box {
+    text-align: right;
+    .price {
+      color: gray;
+      font-size: 16px;
+      text-decoration: line-through;
+    }
+    .disc-price {
+      font-size: 21px;
+      font-weight: bold;
+    }
+  }
+</style>

+ 59 - 0
components/CourseCard.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="card">
+    <div class="card-image">
+      <figure class="image is-4by2">
+        <img :src="course.image" alt="Placeholder image">
+      </figure>
+    </div>
+    <div class="card-content">
+      <div class="media">
+        <div class="media-content">
+          <p class="title is-4">{{course.title | shortenText(45)}}</p>
+          <p class="subtitle is-6"><i>by {{course.author.name}}</i></p>
+        </div>
+      </div>
+      <div class="content">
+        {{course.subtitle | shortenText(45) }}
+        <br>
+      </div>
+      <div class="price-box">
+        <span class="price">{{course.price}}$</span>
+        <span class="disc-price">{{course.discountedPrice}}$</span>
+      </div>
+    </div>
+    <footer class="card-footer">
+      <nuxt-link :to="''" class="card-footer-item">Learn More</nuxt-link>
+      <a target="_" :href="course.productLink" class="card-footer-item">Enroll</a>
+    </footer>
+  </div>
+</template>
+
+<script>
+export default {
+  props : {
+    course : {
+      type : Object,
+      required : true
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .card-image:hover {
+    cursor: pointer;
+    opacity: 0.9;
+  }
+  .price-box {
+    text-align: right;
+    .price {
+      color: gray;
+      font-size: 16px;
+      text-decoration: line-through;
+    }
+    .disc-price {
+      font-size: 21px;
+      font-weight: bold;
+    }
+  }
+</style>

+ 79 - 0
components/Logo.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="VueToNuxtLogo">
+    <div class="Triangle Triangle--two" />
+    <div class="Triangle Triangle--one" />
+    <div class="Triangle Triangle--three" />
+    <div class="Triangle Triangle--four" />
+  </div>
+</template>
+
+<style>
+.VueToNuxtLogo {
+  display: inline-block;
+  animation: turn 2s linear forwards 1s;
+  transform: rotateX(180deg);
+  position: relative;
+  overflow: hidden;
+  height: 180px;
+  width: 245px;
+}
+
+.Triangle {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 0;
+  height: 0;
+}
+
+.Triangle--one {
+  border-left: 105px solid transparent;
+  border-right: 105px solid transparent;
+  border-bottom: 180px solid #41b883;
+}
+
+.Triangle--two {
+  top: 30px;
+  left: 35px;
+  animation: goright 0.5s linear forwards 3.5s;
+  border-left: 87.5px solid transparent;
+  border-right: 87.5px solid transparent;
+  border-bottom: 150px solid #3b8070;
+}
+
+.Triangle--three {
+  top: 60px;
+  left: 35px;
+  animation: goright 0.5s linear forwards 3.5s;
+  border-left: 70px solid transparent;
+  border-right: 70px solid transparent;
+  border-bottom: 120px solid #35495e;
+}
+
+.Triangle--four {
+  top: 120px;
+  left: 70px;
+  animation: godown 0.5s linear forwards 3s;
+  border-left: 35px solid transparent;
+  border-right: 35px solid transparent;
+  border-bottom: 60px solid #fff;
+}
+
+@keyframes turn {
+  100% {
+    transform: rotateX(0deg);
+  }
+}
+
+@keyframes godown {
+  100% {
+    top: 180px;
+  }
+}
+
+@keyframes goright {
+  100% {
+    left: 70px;
+  }
+}
+</style>

+ 7 - 0
components/README.md

@@ -0,0 +1,7 @@
+# COMPONENTS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+The components directory contains your Vue.js Components.
+
+_Nuxt.js doesn't supercharge these components._

+ 83 - 0
components/form/MultiLineTextInput.vue

@@ -0,0 +1,83 @@
+<template>
+  <div>
+    <!-- Send a label through props -->
+    <label class="label">{{label}}</label>
+    <!-- Iterate lines here -->
+    <div class="multi-field field"
+      v-for="(line, index) in lines"
+      :key="line.value">
+      <div class="control multi-control">
+        <div class="multi-input-container">
+          <input
+            :value="line.value"
+            placeholder="Add Something Nice (:"
+            class="input is-medium multi-input"
+            type="text">
+        </div>
+        <div class="btn-container">
+          <!-- Delete the line -->
+          <button
+            @click.prevent="emitRemove(index)"
+            type="button"
+            class="button is-danger multi-button">
+            Delete
+          </button>
+        </div>
+      </div>
+    </div>
+    <!-- Add the Line -->
+    <button
+      @click="emitAdd"
+      type="button"
+      class="m-b-sm button is-medium is-link is-outlined">
+      Add an answer
+    </button>
+  </div>
+</template>
+
+<script>
+export default {
+  props:{
+    label : {
+      type: String,
+      required: true
+    },
+    lines: {
+      type: Array,
+      required: true
+    }
+  },
+  methods: {
+    emitAdd() {
+      this.$emit('addClicked')
+    },
+    emitRemove(index) {
+      this.$emit('removeClicked', index)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.multi-input {
+  float: left;
+  width: 100%;
+}
+.multi-button {
+  height: inherit;
+}
+.multi-input-container {
+  display: flex;
+  flex: 1;
+}
+.btn-container {
+  display: flex;
+  opacity: 0;
+}
+.multi-control {
+  display: flex;
+  &:hover > .btn-container {
+    opacity: 1;
+  }
+}
+</style>

+ 55 - 0
components/form/TextInputWithCount.vue

@@ -0,0 +1,55 @@
+<template>
+  <div>
+    <input
+      :maxLength="maxLength"
+      @input="emitInputValue"
+      @blur="v.$touch()"
+      type="text"
+      placeholder="e.g. Amazing Course in Flutter!"
+      class="input is-large">
+    <span class="icon is-small is-right">
+      {{remainingLength}}
+    </span>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      currentValue: ''
+    }
+  },
+  props: {
+    maxLength: {
+      type: Number,
+      default: 50,
+      required: false
+    },
+    v: {
+      type: Object,
+      required: true,
+    }
+  },
+  computed : {
+    inputLength() {
+      return this.currentValue.length || 0
+    },
+    remainingLength(){
+      if (this.inputLength > 0 && this.inputLength < this.maxLength) {
+        return this.maxLength - this.inputLength
+      } else if (this.inputLength === 0) {
+        return this.maxLength
+      } else {
+        return 0
+      }
+    }
+  },
+  methods : {
+    emitInputValue($event) {
+      this.currentValue = $event.target.value
+      this.$emit('input', this.currentValue)
+    }
+  }
+}
+</script>

+ 59 - 0
components/instructor/CourseCreateStep1.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="course-create-wrapper">
+    <div class="course-create-headerText">
+      Please choose title of your Course.
+    </div>
+    <h2 class="course-create-subtitle">
+      No worries, you can change title later.
+    </h2>
+    <form @input="emitFormData" class="course-create-form">
+      <div class="course-create-form-group">
+        <div class="field course-create-form-field control has-icons-right">
+          <TextInputWithCount
+            v-model="form.title"
+            :v="$v.form.title"
+            :maxLength="50"
+          />
+          <div v-if="$v.form.title.$error" class="form-error">
+            <span v-if="!$v.form.title.required" class="help is-danger">Title is required!</span>
+          </div>
+        </div>
+      </div>
+    </form>
+  </div>
+</template>
+
+<script>
+import { required } from 'vuelidate/lib/validators'
+import TextInputWithCount from '~/components/form/TextInputWithCount'
+
+export default {
+  components: {
+    TextInputWithCount
+  },  
+  data() {
+    return {
+      form : {
+        title : ''
+      }
+    }
+  },
+  validations: {
+    form: {
+      title: {
+        required
+      }
+    }
+  },
+  computed: {
+    isValid() {
+      return !this.$v.$invalid
+    }
+  },
+  methods: {
+    emitFormData() {
+      this.$emit('stepUpdated', {data: this.form, isValid: this.isValid})
+    }
+  }
+}
+</script>

+ 71 - 0
components/instructor/CourseCreateStep2.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="course-create-wrapper">
+    <div class="course-create-headerText">
+      What category best fits the knowledge you'll share?
+    </div>
+    <h2 class="course-create-subtitle">If you're not sure about the right category, you can change it later.</h2>
+    <form class="course-create-form">
+      <div class="course-create-form-group">
+        <div class="course-create-form-field">
+          <div class="select is-large">
+            <select 
+              v-model="form.category"
+              @blur="$v.form.category.$touch()"
+              @change="emitFormData">
+              <option value="default">Select Category</option>
+              <option
+                v-for="category in categories"
+                :key="category._id"
+                :value="category._id">
+                {{category.name}}
+              </option>
+            </select>
+          </div>
+          <div v-if="$v.form.category.$dirty && !isValid" class="form-error">
+            <span class="help is-danger">Category is required!</span>
+          </div>
+        </div>
+      </div>
+    </form>
+  </div>
+</template>
+
+<script>
+import { required } from 'vuelidate/lib/validators'
+export default {
+  data() {
+    return {
+      form: {
+        category: 'default'
+      }
+    }
+  },
+  validations: {
+    form: {
+      category: {
+        required
+      }
+    }
+  },
+  computed: {
+    isValid() {
+      return !this.$v.$invalid && this.form.category !== 'default'
+    },
+    categories() {
+      return this.$store.state.category.items
+    }
+  },
+  methods: {
+    emitFormData() {
+      this.$v.form.$touch()
+      this.$emit('stepUpdated', {data: this.form, isValid: this.isValid})
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .help.is-danger {
+    text-align: left;
+  }
+</style>

+ 85 - 0
components/instructor/LandingPage.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="card manage-card">
+    <header class="card-header card-section">
+      <p class="card-header-title">Course Landing Page</p>
+    </header>
+    <div class="card-content card-section">
+      <form>
+        <div class="field">
+          <label class="label">Course title</label>
+          <div class="control">
+            <input
+              class="input is-medium"
+              type="text"
+              placeholder="Dart and Flutter From Zero to Hero ">
+          </div>
+        </div>
+        <div class="field">
+          <label class="label">Course subtitle</label>
+          <div class="control">
+            <input
+              class="input is-medium"
+              type="text"
+              placeholder="Build real mobile Application for Android and iOS.">
+          </div>
+        </div>
+        <div class="field">
+          <label class="label">Course description</label>
+          <div class="control">
+            <textarea
+              class="textarea is-medium"
+              type="text"
+              placeholder="Write something catchy about the course">
+            </textarea>
+          </div>
+        </div>
+        <div class="field">
+          <label class="label">Category</label>
+          <div class="select is-medium">
+            <select>
+              <option value="default">Select Category</option>
+              <!-- <option> </option> -->
+            </select>
+          </div>
+        </div>
+        <div class="field">
+          <label class="label">Course Image</label>
+          <div class="columns">
+            <div class="column">
+              <figure class="image is-4by2">
+                <img
+                  :src="''">
+              </figure>
+            </div>
+            <div class="column centered">
+              <div class="control">
+                <input
+                  class="input is-medium"
+                  type="text"
+                  placeholder="https://images.unsplash.com/photo-1498837167922-ddd27525d352">
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="field">
+          <label class="label">Course Link</label>
+          <div class="control">
+            <input
+              class="input is-medium"
+              type="text"
+              placeholder="https://www.udemy.com/vue-js-2-the-full-guide-by-real-apps-vuex-router-node">
+          </div>
+        </div>
+        <div class="field">
+          <label class="label">Course Video Link</label>
+          <div class="control">
+            <input
+              class="input is-medium"
+              type="text"
+              placeholder="https://www.youtube.com/watch?v=WQ9sCAhRh1M">
+          </div>
+        </div>
+      </form>
+    </div>
+  </div>
+</template>

+ 27 - 0
components/instructor/Price.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="card manage-card">
+    <header class="card-header card-section">
+      <p class="card-header-title">Pricing</p>
+    </header>
+    <div class="card-content card-section">
+      <div class="field">
+        <label class="label">Price of the course</label>
+        <div class="control">
+          <input
+            class="input is-medium"
+            type="text"
+            placeholder="179.99">
+        </div>
+      </div>
+      <div class="field">
+        <label class="label">Discounted Price for the course</label>
+        <div class="control">
+          <input
+            class="input is-medium"
+            type="text"
+            placeholder="9.99">
+        </div>
+      </div>
+    </div>
+  </div>
+</template>

+ 23 - 0
components/instructor/Status.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="card manage-card">
+    <header class="card-header card-section">
+      <p class="card-header-title">Status</p>
+    </header>
+    <div class="card-content card-section">
+      <div class="field">
+        <label class="label">Status</label>
+        <div class="select is-medium">
+          <select>
+            <option value="default">Change Status</option>
+            <option value="active">
+              Active
+            </option>
+            <option value="published">
+              Published
+            </option>
+          </select>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>

+ 51 - 0
components/instructor/TargetStudents.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="card manage-card">
+    <header class="card-header card-section">
+      <p class="card-header-title">Target your Students</p>
+    </header>
+    <div class="card-content card-section">
+      <form>
+        <multi-line-text-input
+          @addClicked="addLine('wsl')"
+          @removeClicked="removeLine($event, 'wsl')"
+          :lines="course.wsl"
+          label="What will students learn"
+        />
+        <multi-line-text-input
+          @addClicked="addLine('requirements')"
+          @removeClicked="removeLine($event, 'requirements')"
+          :lines="course.requirements"
+          label="What are the requirements"
+        />
+      </form>
+    </div>
+  </div>
+</template>
+
+<script>
+import MultiLineTextInput from "~/components/form/MultiLineTextInput";
+export default {
+  components : {
+    MultiLineTextInput
+  },
+  props :{
+    course: {
+      type: Object,
+      required: true
+    }
+  },
+  methods: {
+    addLine(field) {
+      // Dispatch action to add line
+      console.log('Adding line for: ', field)
+      this.$store.commit('instructor/course/addLine', field)
+    },
+    removeLine(index, field) {
+      // Dispatch action to remove line line
+      console.log('Removing line of index:', index)
+      console.log('Removing line for:', field)
+      this.$store.commit('instructor/course/removeLine', {field, index})
+    }
+  }
+}
+</script>

+ 45 - 0
components/shared/ErrorView.vue

@@ -0,0 +1,45 @@
+<template>
+  <div class="notFoundContainer">
+    <div class="m-b-xxl">
+      <h1 class="title">{{title}}</h1>
+      <nuxt-link :to="navigateToPage" class="button is-primary">{{navigateToText}}</nuxt-link>
+    </div>
+    <h2 class="subtitle">{{status}}<span> :(</span></h2>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    title: {
+      required: true,
+      type: String
+    },
+    status: {
+      required: true,
+      type: String
+    },
+    navigateToPage: {
+      required: true,
+      type: String
+    },
+    navigateToText: {
+      required: true,
+      type: String
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .title {
+    font-size: 40px;
+  }
+  .subtitle {
+    font-size: 140px;
+    text-align: center;
+  }
+  .notFoundContainer {
+    margin-top: 80px;
+  }
+</style>

+ 59 - 0
components/shared/Header.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="full-page-takeover-header">
+    <div class="full-page-takeover-header-logo">
+      <p class="full-page-takeover-header-logo-title">Promo Yourself</p>
+    </div>
+    <div class="full-page-takeover-header-divider">
+    </div>
+    <div class="full-page-takeover-header-text">
+      {{title}}
+    </div>
+    <div class="user-box">
+      <figure class="image is-48x48 m-r-sm">
+        <img class="is-rounded" :src="user.avatar">
+      </figure>
+      <div class="m-r-sm m-b-sm">
+        Welcome {{user.username}}!
+      </div>
+    </div>
+    <slot name="actionMenu"></slot>
+    <div v-if="exitLink" class="full-page-takeover-header-button">
+      <nuxt-link
+        :to="exitLink"
+        class="button is-danger is-medium is-inverted is-outlined">
+        Exit
+      </nuxt-link>
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    title: {
+      required: true,
+      type: String
+    },
+    exitLink: {
+      required: false,
+      type: String,
+      default: null
+    }
+  },
+  computed: {
+    user() {
+      return this.$store.getters['auth/authUser'] || {}
+    }
+  }
+}
+</script>
+<style scoped>
+  .user-box {
+    align-items: center;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+    margin-right: 10px;
+    font-size: 17px;
+    font-weight: bold;
+  }
+</style>

+ 62 - 0
components/shared/Hero.vue

@@ -0,0 +1,62 @@
+<template>
+  <section
+    class="hero is-black is-medium">
+    <div class="hero-body">
+      <div
+        class="hero-img"
+        :style="{ background : `url(https://images.unsplash.com/photo-1510519138101-570d1dca3d66?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1631&q=80) no-repeat center center`}">
+      </div>
+      <div class="container">
+        <h1 class="title">
+          Super Amazing Promo
+        </h1>
+        <h2 class="subtitle">
+          Super Amazing Promo Subtitle
+        </h2>
+        <a target="_" :href="'#'" class="button is-danger">Learn More!</a>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+export default {
+  
+}
+</script>
+
+<style lang="scss" scoped>
+  .hero-body {
+    position: relative;
+  }
+  .hero-img {
+    opacity: 0.8;
+    position: absolute;
+    height: 100%;
+    width: 100%;
+    top: 0;
+    left: 0;
+    -webkit-background-size: cover;
+    -moz-background-size: cover;
+    -o-background-size: cover;
+    background-size: cover;
+  }
+  .user-avatar {
+    display: inline-block;
+  }
+  .is-black {
+    background-color: black;
+  }
+  .title {
+    font-weight: bold;
+    font-size: 45px;
+  }
+  .subtitle {
+    /*font-weight: bold;*/
+    font-size: 25px;
+  }
+  .author-name {
+    font-size: 20px;
+    font-weight: bold;
+  }
+</style>

+ 117 - 0
components/shared/Navbar.vue

@@ -0,0 +1,117 @@
+<template>
+  <nav class="navbar is-active" role="navigation" aria-label="main navigation">
+    <div class="navbar-brand">
+      <nuxt-link class="navbar-item" to="/">
+        <h1 class="brand-title">Promo-Yourself</h1>
+      </nuxt-link>
+      <!-- Adds click to open -->
+      <!-- Adds active class -->
+      <a @click="() => {}"
+         role="button"
+         class="navbar-burger burger"
+         aria-label="menu"
+         aria-expanded="false"
+         data-target="navbarBasicExample">
+        <span aria-hidden="true"></span>
+        <span aria-hidden="true"></span>
+        <span aria-hidden="true"></span>
+      </a>
+    </div>
+
+    <!-- Adds active class -->
+    <div id="navbarBasicExample"
+         class="navbar-menu">
+      <div class="navbar-start">
+        <nuxt-link to="/" class="navbar-item">
+          Home
+        </nuxt-link>
+        <nuxt-link to="#" class="navbar-item">
+          Courses
+        </nuxt-link>
+        <nuxt-link to="#" class="navbar-item">
+          Blogs
+        </nuxt-link>
+        <nuxt-link to="#" class="navbar-item">
+          About
+        </nuxt-link>
+        <nuxt-link to="#" class="navbar-item">
+          Cv
+        </nuxt-link>
+        <!-- <nuxt-link to="/instructor" class="navbar-item">
+          Instructor
+        </nuxt-link>
+        <nuxt-link to="/secret" class="navbar-item">
+          Secret
+        </nuxt-link> -->
+      </div>
+
+      <div class="navbar-end">
+        <div class="navbar-item">
+          <div class="buttons">
+            <!-- If Authenticated -->
+            <template v-if="isAuth">
+              <figure class="image avatar is-48x48 m-r-sm">
+                <img class="is-rounded" :src="user.avatar">
+              </figure>
+              <div class="m-r-sm m-b-sm">
+                Welcome {{user.username}}!
+              </div>
+              <!-- If Admin -->
+              <button
+                 v-if="isAdmin" class="button is-link is-outlined"
+                 @click="() => $router.push('/instructor')">
+                Instructor
+              </button>
+              <a class="button is-primary" @click="logout">
+                Logout
+              </a>
+            </template>
+            <template v-else>
+              <nuxt-link to="/register" class="button is-primary">
+                Sign up
+              </nuxt-link>
+              <nuxt-link to="/login" class="button is-light">
+                Log in
+              </nuxt-link>
+            </template>
+          </div>
+        </div>
+      </div>
+    </div>
+  </nav>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+export default {
+  computed : {
+    ...mapGetters({
+      'user': 'auth/authUser',
+      'isAuth': 'auth/isAuthenticated',
+      'isAdmin': 'auth/isAdmin'
+    })
+  },
+  methods : {
+    async logout() {
+      console.log('Navbar.vue call await this.$store.dispatch(auth/logout)')
+      const result = await this.$store.dispatch('auth/logout')
+      console.log('Navbar.vue done await this.$store.dispatch(auth/logout)', result)
+      this.$router.push('/login')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .brand-title {
+    font-size: 35px;
+    font-weight: bold;
+  }
+  .navbar-brand {
+    padding-right: 30px;
+  }
+  .avatar {
+    margin-right: 5px;
+  }
+</style>

+ 12 - 0
helpers/validators.js

@@ -0,0 +1,12 @@
+
+
+import { helpers } from "vuelidate/lib/validators";
+
+export const supportedFileType = (value) => {
+  
+  if(!helpers.req(value)) return true
+  
+  const allowedFormats = ['jpg', 'png', 'jpeg']
+  const extension = value.split('.').pop()
+  return allowedFormats.includes(extension)
+}

+ 7 - 0
layouts/README.md

@@ -0,0 +1,7 @@
+# LAYOUTS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your Application Layouts.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts).

+ 65 - 0
layouts/default.vue

@@ -0,0 +1,65 @@
+<template>
+  <div>
+    <Navbar/>
+    <nuxt />
+  </div>
+</template>
+<script>
+import Navbar from "~/components/shared/Navbar";
+export default {
+  components : {
+    Navbar
+  }
+}
+</script>
+
+
+<style>
+html {
+  font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
+    Roboto, 'Helvetica Neue', Arial, sans-serif;
+  font-size: 16px;
+  word-spacing: 1px;
+  -ms-text-size-adjust: 100%;
+  -webkit-text-size-adjust: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  box-sizing: border-box;
+}
+
+*,
+*:before,
+*:after {
+  box-sizing: border-box;
+  margin: 0;
+}
+
+.button--green {
+  display: inline-block;
+  border-radius: 4px;
+  border: 1px solid #3b8070;
+  color: #3b8070;
+  text-decoration: none;
+  padding: 10px 30px;
+}
+
+.button--green:hover {
+  color: #fff;
+  background-color: #3b8070;
+}
+
+.button--grey {
+  display: inline-block;
+  border-radius: 4px;
+  border: 1px solid #35495e;
+  color: #35495e;
+  text-decoration: none;
+  padding: 10px 30px;
+  margin-left: 15px;
+}
+
+.button--grey:hover {
+  color: #fff;
+  background-color: #35495e;
+}
+</style>

+ 71 - 0
layouts/instructor.vue

@@ -0,0 +1,71 @@
+<template>
+  <nuxt />
+</template>
+<script>
+export default {
+  middleware: 'admin',
+}
+</script>
+<style lang="scss">
+html {
+  font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
+    Roboto, 'Helvetica Neue', Arial, sans-serif;
+  color: #505763;;
+  background-color: #f2f3f5;
+  font-size: 16px;
+  word-spacing: 1px;
+  -ms-text-size-adjust: 100%;
+  -webkit-text-size-adjust: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  box-sizing: border-box;
+}
+*,
+*:before,
+*:after {
+  box-sizing: border-box;
+  margin: 0;
+}
+.card.manage-card {
+  .label {
+    color: #353535;
+  }
+  .card-header-title {
+    color: #616161;
+  }
+}
+.full-page-takeover-header {
+  background-color: #58529f;
+  color: white;
+  &-text {
+    color: white;
+    font-weight: bold;
+    font-size: 22px;
+  }
+}
+.button--green {
+  display: inline-block;
+  border-radius: 4px;
+  border: 1px solid #3b8070;
+  color: #3b8070;
+  text-decoration: none;
+  padding: 10px 30px;
+}
+.button--green:hover {
+  color: #fff;
+  background-color: #3b8070;
+}
+.button--grey {
+  display: inline-block;
+  border-radius: 4px;
+  border: 1px solid #35495e;
+  color: #35495e;
+  text-decoration: none;
+  padding: 10px 30px;
+  margin-left: 15px;
+}
+.button--grey:hover {
+  color: #fff;
+  background-color: #35495e;
+}
+</style>

+ 8 - 0
middleware/README.md

@@ -0,0 +1,8 @@
+# MIDDLEWARE
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your application middleware.
+Middleware let you define custom functions that can be run before rendering either a page or a group of pages.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).

+ 9 - 0
middleware/admin.js

@@ -0,0 +1,9 @@
+export default function({store, redirect}) {
+  const isAdmin = store.getters['auth/isAdmin']
+
+  if (!isAdmin) {
+    // navigate later to notAuthorized page
+    // return redirect('/')
+    return redirect('/notAuthorized')
+  }
+}

+ 10 - 0
middleware/auth.js

@@ -0,0 +1,10 @@
+export default function({store, redirect}) {
+  const isAuth = store.getters['auth/isAuthenticated']
+
+  if (!isAuth) {
+    // navigate later to notAuthenticated page
+    // return redirect('/')
+    return redirect('/notAuthenticated')
+  }
+}
+

+ 7 - 0
middleware/guest.js

@@ -0,0 +1,7 @@
+export default function({store, redirect}) {
+  const isAuth = store.getters['auth/isAuthenticated']
+
+  if (isAuth) {
+    return redirect('/')
+  }
+}

+ 39 - 0
mixins/MultiComponentMixin.js

@@ -0,0 +1,39 @@
+export default {
+  data() {
+    return {
+      activeStep: 1,
+      steps: []
+    }
+  },
+  computed : {
+    stepsLength() {
+      return this.steps.length
+    },
+    isLastStep() {
+      return this.activeStep === this.stepsLength
+    },
+    isFirstStep() {
+      return this.activeStep === 1
+    },
+    progress() {
+      return `${100 / this.stepsLength * this.activeStep}%`
+    },
+    activeComponent() {
+      return this.steps[this.activeStep - 1]
+    }
+  },
+  methods:{
+    nextStep (){
+      this.activeStep++
+    },
+    previousStep (){
+      this.activeStep--
+    },
+    navigateTo(step){
+      this.activeStep = step
+    },
+    activeComponentClass(step){
+      return this.activeStep === step ? 'is-active' : ''
+    }
+  }
+}

+ 62 - 0
nuxt.config.js

@@ -0,0 +1,62 @@
+
+module.exports = {
+  mode: 'universal',
+  /*
+  ** Headers of the page
+  */
+  head: {
+    title: process.env.npm_package_name || '',
+    meta: [
+      { charset: 'utf-8' },
+      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
+      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
+    ],
+    link: [
+      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
+    ]
+  },
+  /*
+  ** Customize the progress-bar color
+  */
+  loading: { color: '#fff' },
+  /*
+  ** Global CSS
+  */
+  css: [
+    '@/assets/scss/main.scss'
+  ],
+  /*
+  ** Plugins to load before mounting the App
+  */
+  plugins: [
+    {src: '~/plugins/filters'},
+    {src: '~/plugins/vuelidate'},
+    {src: '~/plugins/toasted', ssr: false},
+  ],
+  /*
+  ** Nuxt.js modules
+  */
+  modules: [
+    // Doc: https://axios.nuxtjs.org/usage
+    '@nuxtjs/axios',
+  ],
+  /*
+  ** Axios module configuration
+  ** See https://axios.nuxtjs.org/options
+  */
+  axios: {
+  },
+  serverMiddleware: [
+    '~/server/routes/index'
+  ],
+  /*
+  ** Build configuration
+  */
+  build: {
+    /*
+    ** You can extend webpack config here
+    */
+    extend(config, ctx) {
+    }
+  }
+}

File diff suppressed because it is too large
+ 12105 - 0
package-lock.json


+ 44 - 0
package.json

@@ -0,0 +1,44 @@
+{
+  "name": "nuxt-promo-yourself",
+  "version": "1.0.0",
+  "description": "My super Nuxt.js project",
+  "author": "yongun",
+  "private": true,
+  "scripts": {
+    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
+    "build": "nuxt build",
+    "start": "cross-env NODE_ENV=production node server/index.js",
+    "generate": "nuxt generate"
+  },
+  "dependencies": {
+    "cross-env": "^5.2.0",
+    "express": "^4.16.4",
+    "@nuxtjs/axios": "^5.3.6",
+    "nuxt": "^2.0.0",
+    "async": "^3.0.1",
+    "async-lock": "^1.2.0",
+    "bcrypt": "^3.0.6",
+    "body-parser": "^1.19.0",
+    "bulma": "^0.7.5",
+    "connect-mongodb-session": "^2.2.0",
+    "express-session": "^1.16.2",
+    "highlight.js": "^9.15.8",
+    "jsonwebtoken": "^8.5.1",
+    "moment": "^2.24.0",
+    "mongoose": "^5.6.0",
+    "passport": "^0.4.0",
+    "passport-local": "^1.0.0",
+    "portal-vue": "^2.1.5",
+    "slugify": "^1.3.4",
+    "tiptap": "^1.23.0",
+    "tiptap-extensions": "^1.24.0",
+    "vue-toasted": "^1.1.27",
+    "vuejs-paginate": "^2.1.0",
+    "vuelidate": "^0.7.4"
+  },
+  "devDependencies": {
+    "nodemon": "^1.18.9",
+    "node-sass": "^4.12.0",
+    "sass-loader": "^7.1.0"
+  }
+}

+ 6 - 0
pages/README.md

@@ -0,0 +1,6 @@
+# PAGES
+
+This directory contains your Application Views and Routes.
+The framework reads all the `*.vue` files inside this directory and creates the router of your application.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing).

+ 120 - 0
pages/index.vue

@@ -0,0 +1,120 @@
+<template>
+  <div>
+    <!-- HERO -->
+    <Hero/>
+    <!-- HERO -->
+    <section class="section">
+      <div class="container">
+        <h1 class="title">Featured Courses</h1>
+        <div class="columns">
+          <div 
+            v-for="course in courses"
+            :key="course._id"
+            class="column is-one-quarter">
+            <!-- CARD-ITEM -->
+            <CourseCard :course="course"/>
+            <!-- CARD-ITEM-END -->
+          </div>
+        </div>
+      </div>
+    </section>
+    <section class="section">
+      <div class="container">
+        <h1 class="title">Featured Articles</h1>
+        <div class="columns">
+          <div class="column is-one-quarter">
+            <!-- CARD-ITEM -->
+            <BlogCard/>
+            <!-- CARD-ITEM-END -->
+          </div>
+        </div>
+      </div>
+    </section>
+  </div>
+</template>
+
+<script>
+
+import Hero from "~/components/shared/Hero";
+import CourseCard from "~/components/CourseCard";
+import BlogCard from "~/components/BlogCard";
+import {mapState} from "vuex"
+export default {
+  components: {
+    Hero, CourseCard, BlogCard
+  },
+  computed : {
+    ...mapState({
+      // courses: state => { 
+      //   console.log('index.vue mapState')
+      //   return state.course.items
+      // }
+      courses : state => state.course.items
+    })
+  },
+  async fetch({store}) {
+    const result = await store.dispatch('course/fetchCourses')
+    console.log('index.vue fetch')
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  // card item
+  .card-image:hover {
+    cursor: pointer;
+    opacity: 0.9;
+  }
+  .price-box {
+    text-align: right;
+    .price {
+      color: gray;
+      font-size: 16px;
+      text-decoration: line-through;
+    }
+    .disc-price {
+      font-size: 21px;
+      font-weight: bold;
+    }
+  }
+  // card item end
+  // hero
+  .hero-body {
+    position: relative;
+  }
+  .hero-img {
+    opacity: 0.8;
+    position: absolute;
+    height: 100%;
+    width: 100%;
+    top: 0;
+    left: 0;
+    -webkit-background-size: cover;
+    -moz-background-size: cover;
+    -o-background-size: cover;
+    background-size: cover;
+  }
+  .user-avatar {
+    display: inline-block;
+  }
+  .is-black {
+    background-color: black;
+  }
+  .title {
+    font-weight: bold;
+    font-size: 45px;
+  }
+  .subtitle {
+    /*font-weight: bold;*/
+    font-size: 25px;
+  }
+  .author-name {
+    font-size: 20px;
+    font-weight: bold;
+  }
+  // hero
+  // Home page
+  .links {
+    padding-top: 15px;
+  }
+</style>

+ 163 - 0
pages/instructor/course/_id/manage.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="manage-page">
+    <Header
+      title="Some very nice course name"
+      exitLink="/instructor/courses">
+      <template #actionMenu>
+        <div class="full-page-takeover-header-button">
+          <button
+            @click="() => {}"
+            class="button is-primary is-inverted is-medium is-outlined">
+            Save
+          </button>
+        </div>
+      </template>
+    </Header>
+    <div class="course-manage">
+      <div class="container">
+        <div class="columns">
+          <div class="column is-3 p-lg">
+            <!-- <aside class="menu is-hidden-mobile"> -->
+            <aside class="menu">
+              <p class="menu-label">
+                Course Editing
+              </p>
+              <ul class="menu-list">
+                <li>
+                  <!-- display TargetStudents -->
+                  <a @click.prevent="navigateTo(1)"
+                     :class="activeComponentClass(1)">Target Your Students
+                  </a>
+                </li>
+                <li>
+                  <!-- display LandingPage -->
+                  <a 
+                    :class="activeComponentClass(2)"
+                    @click.prevent="navigateTo(2)">
+                    Course Landing Page
+                  </a>
+                </li>
+              </ul>
+              <p class="menu-label">
+                Course Managment
+              </p>
+              <ul class="menu-list">
+                <li>
+                  <!-- display Price -->
+                  <a 
+                    :class="activeComponentClass(3)"
+                    @click.prevent="navigateTo(3)">
+                    Price
+                  </a>
+                </li>
+                <li>
+                  <!-- display Status -->
+                  <a 
+                    :class="activeComponentClass(4)"
+                    @click.prevent="navigateTo(4)">
+                    Status
+                  </a>
+                </li>
+              </ul>
+            </aside>
+          </div>
+          <div class="column">
+            <!-- 
+            <TargetStudents/>
+            <LandingPage/>
+            <Price/>
+            <Status/> -->
+            <keep-alive>
+              <component 
+              :is="activeComponent"
+              :course="course"/>
+            </keep-alive>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Header from '~/components/shared/Header'
+import TargetStudents from '~/components/instructor/TargetStudents'
+import LandingPage from '~/components/instructor/LandingPage'
+import Price from '~/components/instructor/Price'
+import Status from '~/components/instructor/Status'
+import MultiComponentMixin from '~/mixins/MultiComponentMixin'
+import { mapState } from 'vuex'
+
+export default {
+  layout: 'instructor',
+  components: { 
+    Header,
+    TargetStudents,
+    LandingPage,
+    Price,
+    Status
+  },
+  mixins: [MultiComponentMixin],
+  data() {
+    return {
+      steps : ['TargetStudents', 'LandingPage', 'Price', 'Status'],
+    }
+  },
+  async fetch({store, params}) {
+    console.log('course/_id/manage.vue fetch call')
+    const result = await store.dispatch('instructor/course/fetchCourseById', params.id)
+    console.log('course/_id/manage.vue fetch done', result)
+  },
+  computed: {
+    ...mapState({
+      course: ({instructor}) => instructor.course.item
+    })
+  }
+}
+</script>
+
+<style lang="scss">
+  .manage-page {
+    .label-info {
+      font-size: 13px;
+      color: gray;
+      font-style: italic;
+    }
+    .course-manage {
+      padding-top: 40px;
+      .menu {
+        padding-top: 30px;
+        &-label {
+          font-size: 20px;
+          color: black;
+        }
+        &-list {
+          >li {
+            font-size: 18px;
+            margin-top: 10px;
+            > a {
+              &.is-active {
+                border-left: 3px solid #58529f;
+                background-color: transparent;
+                color: inherit;
+              }
+            }
+          }
+        }
+      }
+      .card {
+        &-section {
+          padding: 40px;
+        }
+        &-header {
+          &-title {
+            padding: 0;
+            color: #8F99A3;
+            font-weight: 400;
+            font-size: 25px;
+          }
+        }
+      }
+    }
+  }
+</style>

+ 159 - 0
pages/instructor/course/create.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class="full-page-takeover-window">
+    <div class="full-page-takeover-page">
+      <Header
+        :title="`Step ${activeStep} of ${stepsLength}`"
+        exitLink="/instructor/courses" />
+      <div class="full-page-takeover-header-bottom-progress">
+        <div :style="{width: progress}"
+             class="full-page-takeover-header-bottom-progress-highlight">
+        </div>
+      </div>
+      <div class="course-create full-page-takeover-container">
+        <div class="container">
+          <!-- <CourseCreateStep1 v-if="activeStep === 1"/>
+          <CourseCreateStep2 v-if="activeStep === 2"/> -->
+          <keep-alive>
+            <component 
+              @stepUpdated="mergeFormData"
+              :is="activeComponent"
+              ref="activeComponent"/>
+          </keep-alive>
+        </div>
+        <div class="full-page-footer-row">
+          <div class="container">
+            <div class="full-page-footer-col">
+              <div v-if="!isFirstStep">
+                <a @click.prevent="_previousStep" class="button is-large">Previous</a>
+              </div>
+              <div v-else class="empty-container">
+              </div>
+            </div>
+            <div class="full-page-footer-col">
+              <div>
+                <button
+                  v-if="!isLastStep"
+                  :disabled="!canProceed"
+                  @click.prevent="_nextStep"
+                  class="button is-large float-right">
+                  Continue
+                </button>
+                <button
+                  v-else
+                  :disabled="!canProceed"
+                  @click="createCourse"
+                  class="button is-success is-large float-right">
+                  Confirm
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Header from '~/components/shared/Header'
+import CourseCreateStep1 from '~/components/instructor/CourseCreateStep1'
+import CourseCreateStep2 from '~/components/instructor/CourseCreateStep2'
+import MultiComponentMixin from '~/mixins/MultiComponentMixin'
+
+export default {
+  layout: 'instructor',
+  components: {
+    Header,
+    CourseCreateStep1,
+    CourseCreateStep2,
+  },
+  mixins: [MultiComponentMixin],
+  data() {
+    return {
+      activeStep : 1,
+      steps: ['CourseCreateStep1', 'CourseCreateStep2'],
+      canProceed: false,
+      form: {
+        title: '',
+        category: ''
+      }
+    }
+  },
+  methods : {
+    _nextStep (){
+      // this.activeStep++
+      // this.canProceed = false
+      this.nextStep()
+      this.$nextTick(() => this.canProceed = this.$refs.activeComponent.isValid)
+      // this.canProceed = this.$refs.activeComponent.isValid
+    },
+    _previousStep (){
+      // this.activeStep--
+      // this.canProceed = true
+      this.previousStep()
+    },
+    mergeFormData({data, isValid}) {
+      this.form = {...this.form, ...data}
+      this.canProceed = isValid
+    },
+    async createCourse() {
+      console.log('course/create.vue createCourse call store.dispatch-createCourse')
+      const result = await this.$store.dispatch('instructor/course/createCourse', this.form)
+      console.log('course/create.vue createCourse done store.dispatch-createCourse')
+      this.$router.push('/instructor/courses')
+    }
+  },
+  async fetch({store}) {
+    console.log('create.vue call fetch')
+    await store.dispatch('category/fetchCategories')
+    console.log('create.vue done fetch')
+  },
+}
+</script>
+
+<style lang="scss">
+  .float-right {
+    float: right;
+  }
+  .empty-container {
+    width: 1px;
+    height: 1px;
+  }
+  .course-create {
+    &-wrapper {
+      margin-top: 60px;
+      text-align: center;
+    }
+    &-headerText {
+      font-weight: 500;
+      line-height: 1.1;
+      margin-top: 21px;
+      margin-bottom: 10.5px;
+      font-size: 36px;
+      font-weight: 300;
+    }
+    &-subtitle {
+      font-size: 24px;
+      font-weight: 300;
+      margin-top: 21px;
+      margin-bottom: 10.5px;
+    }
+    &-form {
+      margin-top: 60px;
+      &-group {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+      }
+      &-field {
+        min-width: 552px;
+      }
+      .select {
+        width: 100%;
+        >select {
+          width: 100%;
+        }
+      }
+    }
+  }
+</style>

+ 3 - 0
pages/instructor/course/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <h1>Create Course Page</h1>
+</template>

+ 155 - 0
pages/instructor/courses/index.vue

@@ -0,0 +1,155 @@
+<template>
+  <div>
+    <instructor-header title="Create your courses">
+      <template #actionMenu>
+        <div class="full-page-takeover-header-button">
+          <nuxt-link
+            to="/instructor/course/create"
+            class="button is-medium is-light">
+            New Course
+          </nuxt-link>
+          <nuxt-link
+            to="/"
+            class="button is-danger is-medium is-inverted is-outlined">
+            Student
+          </nuxt-link>
+        </div>
+      </template>
+    </instructor-header>
+    <div class="courses-page">
+      <div class="container">
+        <div class="columns">
+          <div class="column is-8 is-offset-2">
+            <h1 class="courses-page-title">Your Courses</h1>
+            <!-- Iterate Courses -->
+            <div 
+              v-for="course in courses"
+              :key="course._id"
+              class="tile is-ancestor">
+              <div class="tile is-parent is-12">
+                <!-- Navigate to course manage page -->
+                <nuxt-link :to="`/instructor/course/${course._id}/manage`" class="tile tile-overlay-container is-child box">
+                  <div class="tile-overlay">
+                    <span class="tile-overlay-text">
+                      Update Course
+                    </span>
+                  </div>
+                  <div class="columns">
+                    <div class="column is-narrow">
+                      <figure class="image is-4by2 is-128x128">
+                        <img :src="course.image || 'https://via.placeholder.com/150'">
+                      </figure>
+                    </div>
+                    <div class="column">
+                      <p class="title">{{course.title}}</p>
+                      <p class="subtitle">{{course.subtitle || 'No subtitle provided yet'}}</p>
+                      <span class="tag"
+                            :class="createStatusClass(course.status)">Published</span>
+                    </div>
+                    <div class="column is-narrow flex-centered">
+                      <div class="price-title">
+                        {{course.price || 0}} $
+                      </div>
+                    </div>
+                  </div>
+                </nuxt-link>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import InstructorHeader from '~/components/shared/Header'
+export default {
+  layout: 'instructor',
+  components: {
+    InstructorHeader
+  },
+  computed : {
+    courses(){
+      return this.$store.state.instructor.course.items
+    }
+  },
+  async fetch({store}){
+    console.log('courses/index.vue fetch call')
+    const  result = await store.dispatch('instructor/course/fetchInstructorCourses')
+    console.log('courses/index.vue fetch done', result)
+  },
+  methods: {
+    createStatusClass(status) {
+      if (!status) return ''
+      if (status === 'published') return 'is-success'
+      if (status === 'active') return 'is-primary'
+      if (status === 'inactive') return 'is-warning'
+      if (status === 'deleted') return 'is-danger'
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+  .tile-image {
+    float: left;
+  }
+  .price-title {
+    font-size: 40px;
+    font-weight: bold;
+  }
+  .flex-centered {
+    align-items: center;
+    justify-content: flex-end;
+    display: flex;
+  }
+  .tile-overlay-container {
+    position: relative;
+    &:hover {
+      box-shadow: none;
+    }
+    &:hover > .tile-overlay {
+      background-color: rgba(255,255,255,.9);
+    }
+    &:hover .tile-overlay-text {
+      visibility: visible;
+    }
+    .tile-overlay {
+      position: absolute;
+      display: block;
+      height: auto;
+      bottom: 0;
+      width: auto;
+      right: 0;
+      top: 0;
+      left: 0;
+      z-index: 2;
+      cursor: pointer;
+      &-text {
+        color: #58529f;
+        visibility: hidden;
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        font-size: 18px;
+        white-space: pre-wrap;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        justify-content: center;
+        font-size: 30px;
+        font-weight: 700;
+        white-space: nowrap;
+      }
+    }
+  }
+  .courses-page {
+    padding-top: 60px;
+    &-title {
+      font-size: 40px;
+      font-weight: bold;
+      padding-bottom: 20px;
+    }
+  }
+</style>

+ 67 - 0
pages/instructor/index.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="instructor-page">
+    <instructor-header
+      title="Choose your admin page"
+      exitLink="/"/>    
+    <div class="centered">
+      <div class="columns">
+        <!-- Go to /instructor/courses -->
+        <div class="box" @click="() => $router.push('/instructor/courses')">
+          <div>
+            Courses
+          </div>
+        </div>
+        <!-- Go to /instructor/blogs -->
+        <div class="box" @click="() => {}">
+          <div>
+            Blogs
+          </div>
+        </div>
+        <!-- Go to /instructor/heroes -->
+        <div class="box" @click="() => {}">
+          <div>
+            Heroes
+          </div>
+        </div>
+      </div>
+    </div>-
+  </div>
+</template>
+<script>
+import InstructorHeader from '~/components/shared/Header'
+export default {
+  layout: 'instructor',
+  components: {
+    InstructorHeader
+  }
+}
+</script>
+<style scoped lang="scss">
+  .instructor-page {
+    .centered {
+      margin-top: 100px;
+      display: flex;
+      flex-direction: row;     /* make main axis horizontal (default setting) */
+      justify-content: center; /* center items horizontally, in this case */
+      align-items: center;     /* center items vertically, in this case */
+    }
+  }
+  .box {
+    height: 300px;
+    width: 300px;
+    display: flex;
+    margin: 5px;
+    justify-content: center;
+    &:hover {
+      cursor: pointer;
+      background-color: #58529f;
+      color: white;
+      transition: background-color 0.3s ease-out;
+    }
+    > div {
+      align-self: center;
+      font-size: 50px;
+      font-weight: bold;
+    }
+  }
+</style>

+ 139 - 0
pages/login.vue

@@ -0,0 +1,139 @@
+<template>
+  <section class="hero is-success is-fullheight">
+    <div class="hero-body">
+      <div class="container has-text-centered">
+        <div class="column is-4 is-offset-4">
+          <h3 class="title has-text-grey">Login</h3>
+          <p class="subtitle has-text-grey">Please login to proceed.</p>
+          <div class="box">
+            <figure class="avatar">
+              <img src="https://via.placeholder.com/300">
+            </figure>
+            <form>
+              <div class="field">
+                <div class="control">
+                  <input class="input is-large"
+                    @blur="$v.form.email.$touch()"
+                    v-model="form.email"
+                    type="email"
+                    placeholder="Your Email"
+                    autofocus=""
+                    autocomplete="email">
+                  <div v-if="$v.form.email.$error" class="form-error">
+                    <span v-if="!$v.form.email.required" class="help is-danger">Email is required</span>
+                    <span v-if="!$v.form.email.emailValidator" class="help is-danger">Email address is not valid</span>
+                  </div>
+                </div>
+              </div>
+              <div class="field">
+                <div class="control">
+                  <input
+                    @blur="$v.form.password.$touch()"
+                    v-model="form.password"
+                    class="input is-large"
+                    type="password"
+                    placeholder="Your Password"
+                    autocomplete="current-password">
+                  <div v-if="$v.form.password.$error" class="form-error">
+                    <span v-if="!$v.form.password.required" class="help is-danger">Password is required</span>
+                  </div>
+                </div>
+              </div>
+              <!-- Login Button -->
+              <button
+                @click.prevent="login"
+                :disabled="$v.form.$invalid"
+                class="button is-block is-info is-large is-fullwidth">
+                Login
+              </button>
+            </form>
+          </div>
+          <p class="has-text-grey">
+            <a>Sign In With Google</a> &nbsp;·&nbsp;
+            <nuxt-link to="/register">Sign Up</nuxt-link> &nbsp;·&nbsp;
+
+            <a href="../">Need Help?</a>
+          </p>
+        </div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+import { required, email } from 'vuelidate/lib/validators'
+
+export default {
+  middleware: 'guest',
+  data() {
+    return {
+      form : {
+        email : null,
+        password : null
+      }
+    }
+  },
+  validations: {
+    form: {
+      email: {
+        emailValidator : email,
+        required
+      },
+      password: {
+        required
+      }
+    }
+  },
+  computed : {
+    isFormValid() {
+      return !this.$v.$invalid
+    }
+  },
+  methods: {
+    async login() {
+      // console.log(this.form)
+      this.$v.form.$touch()
+      if(this.isFormValid){
+        const result = await this.$store.dispatch('auth/login', this.form)
+        console.log('login.vue login done this.$store.dispatch');
+        console.log(result);
+        
+        if(result.isAxiosError === true){
+          this.$toasted.error('Wrong email or password', {duration: 3000})
+        }else{
+          this.$router.push('/')
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .hero.is-success {
+    background: #F2F6FA;
+  }
+  .hero .nav, .hero.is-success .nav {
+    -webkit-box-shadow: none;
+    box-shadow: none;
+  }
+  .box {
+    margin-top: 5rem;
+  }
+  .avatar {
+    margin-top: -70px;
+    padding-bottom: 20px;
+  }
+  .avatar img {
+    height: 128px;
+    width: 128px;
+    padding: 5px;
+    background: #fff;
+    border-radius: 50%;
+    -webkit-box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1);
+    box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1);
+  }
+  p.subtitle {
+    padding-top: 1rem;
+  }
+</style>

+ 20 - 0
pages/notAuthenticated.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="container">
+    <ErrorView :title="'Ooooops, you are not authenticated to visit this page, please login'"
+               :status="'401'"
+               :navigateToPage="'login'"
+               :navigateToText="'Navigate to Login Page'" />
+  </div>
+</template>
+
+<script>
+import ErrorView from '@/components/shared/ErrorView'
+export default {
+  components: {
+    ErrorView
+  }
+}
+</script>
+
+<style scoped>
+</style>

+ 20 - 0
pages/notAuthorized.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="container">
+    <ErrorView :title="'Ooooops, you are not authorized to visit this page, please talk to administrator in order to get access rights'"
+               :status="'401'"
+               :navigateToPage="'/'"
+               :navigateToText="'Navigate to Home Page'" />
+  </div>
+</template>
+
+<script>
+import ErrorView from '@/components/shared/ErrorView'
+export default {
+  components: {
+    ErrorView
+  }
+}
+</script>
+
+<style scoped>
+</style>

+ 212 - 0
pages/register.vue

@@ -0,0 +1,212 @@
+<template>
+  <section class="hero is-success is-fullheight">
+    <div class="hero-body">
+      <div class="container has-text-centered">
+        <div class="column is-4 is-offset-4">
+          <h3 class="title has-text-grey">Register</h3>
+          <p class="subtitle has-text-grey">Please register to proceed.</p>
+          <div class="box">
+            <figure class="avatar">
+              <img src="https://via.placeholder.com/300">
+            </figure>
+            <form>
+              <div class="field">
+                <div class="control">
+                  <input
+                    v-model="form.username"
+                    @blur="$v.form.username.$touch()"
+                    class="input is-large"
+                    type="text"
+                    placeholder="Username">
+                  <div v-if="$v.form.username.$error" class="form-error">
+                    <span v-if="!$v.form.username.required" class="help is-danger">Username is required</span>
+                  </div>
+                </div>
+              </div>
+              <div class="field">
+                <div class="control">
+                  <input
+                    v-model="form.name"
+                    @blur="$v.form.name.$touch()"
+                    class="input is-large"
+                    type="text"
+                    placeholder="Name">
+                  <div v-if="$v.form.name.$error" class="form-error">
+                    <span v-if="!$v.form.name.required" class="help is-danger">Name is required</span>
+                    <span v-if="!$v.form.name.minLength" class="help is-danger">Name minimum length is 6</span>
+                  </div>
+                </div>
+              </div>
+              <div class="field">
+                <div class="control">
+                  <input
+                    v-model="form.email"
+                    @blur="$v.form.email.$touch()"
+                    class="input is-large"
+                    type="email"
+                    placeholder="Your Email">
+                  <div v-if="$v.form.email.$error" class="form-error">
+                    <span v-if="!$v.form.email.required" class="help is-danger">Email is required</span>
+                    <span v-if="!$v.form.email.emailValidator" class="help is-danger">Email address is not valid</span>
+                  </div>
+                </div>
+              </div>
+              <div class="field">
+                <div class="control">
+                  <input
+                    v-model="form.avatar"
+                    @blur="$v.form.avatar.$touch()"
+                    class="input is-large"
+                    type="text"
+                    placeholder="Avatar"
+                    autocomplete="">
+                  <div v-if="$v.form.avatar.$error" class="form-error">
+                    <span v-if="!$v.form.avatar.url" class="help is-danger">Url format is not valid!</span>
+                    <span v-if="!$v.form.avatar.supportedFileType" class="help is-danger">Selected file type is not valid!</span>
+                  </div>
+                </div>
+              </div>
+              <div class="field">
+                <div class="control">
+                  <input
+                    v-model="form.password"
+                    @blur="$v.form.password.$touch()"
+                    class="input is-large"
+                    type="password"
+                    placeholder="Your Password"
+                    autocomplete="new-password">
+                  <div v-if="$v.form.password.$error" class="form-error">
+                    <span v-if="!$v.form.password.required" class="help is-danger">Password is required</span>
+                    <span v-if="!$v.form.password.minLength" class="help is-danger">Password minimum length is 6 letters</span>
+                  </div>
+                </div>
+              </div>
+              <div class="field">
+                <div class="control">
+                  <input
+                    v-model="form.passwordConfirmation"
+                    @blur="$v.form.passwordConfirmation.$touch()"
+                    class="input is-large"
+                    type="password"
+                    placeholder="Password Confirmation"
+                    autocomplete="off">
+                  <div v-if="$v.form.passwordConfirmation.$error" class="form-error">
+                    <span v-if="!$v.form.passwordConfirmation.required" class="help is-danger">Password is required</span>
+                    <span v-if="!$v.form.passwordConfirmation.sameAs" class="help is-danger">Password confirmation should be the same as password</span>
+                  </div>
+                </div>
+              </div>
+              <button @click.prevent="register" type="button" class="button is-block is-info is-large is-fullwidth">Register</button>
+            </form>
+          </div>
+          <p class="has-text-grey">
+            <nuxt-link to="/login">Login</nuxt-link> &nbsp;·&nbsp;
+            <a>Sign Up With Google</a> &nbsp;·&nbsp;
+            <a href="../">Need Help?</a>
+          </p>
+        </div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script>
+import { required, email, sameAs, minLength, url } from 'vuelidate/lib/validators'
+import { supportedFileType } from '@/helpers/validators'
+
+export default {
+  middleware: 'guest',
+  data() {
+    return {
+      form : {
+        username : null,
+        name : null,
+        email : null,
+        avatar : null,
+        password : null,
+        passwordConfirmation : null
+      }
+    }
+  },
+  validations: {
+    form: {
+        username : {
+          required,
+          minLength: minLength(6)
+        },
+        name : {
+          required,
+          minLength: minLength(6)
+        },
+        email : {
+          emailValidator : email,
+          required
+        },
+        avatar : {
+          url,
+          supportedFileType
+        },
+        password : {
+          required,
+          minLength: minLength(6)
+        },
+        passwordConfirmation : {
+          required,
+          sameAs: sameAs('password')
+        },
+    }
+  },
+  computed : {
+    isFormValid() {
+      return !this.$v.form.$invalid
+    }
+  },
+  methods : {
+    async register() {
+      console.log(this.form)
+      this.$v.form.$touch()
+      if(this.isFormValid){
+          console.log('register.vue call await this.$store.dispatch(auth/register)')
+          const result = await this.$store.dispatch('auth/register', this.form)
+          console.log('register.vue done await this.$store.dispatch(auth/register)', result)
+
+          if(result.isAxiosError === true){
+            console.log('register.vue error await this.$store.dispatch(auth/register)')
+            this.$toasted.error(result.response.data.errors.message, {duration: 3000})
+          }else{
+            this.$router.push('/login')
+          }
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .hero.is-success {
+    background: #F2F6FA;
+  }
+  .hero .nav, .hero.is-success .nav {
+    -webkit-box-shadow: none;
+    box-shadow: none;
+  }
+  .box {
+    margin-top: 5rem;
+  }
+  .avatar {
+    margin-top: -70px;
+    padding-bottom: 20px;
+  }
+  .avatar img {
+    height: 128px;
+    width: 128px;
+    padding: 5px;
+    background: #fff;
+    border-radius: 50%;
+    -webkit-box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1);
+    box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1);
+  }
+  p.subtitle {
+    padding-top: 1rem;
+  }
+</style>

+ 14 - 0
pages/secret.vue

@@ -0,0 +1,14 @@
+<template>
+  <div>
+    <h1> I AM SUPER SECRET PAGE</h1>
+    <h1> I AM SUPER SECRET PAGE</h1>
+    <h1> I AM SUPER SECRET PAGE</h1>
+    <h1> I AM SUPER SECRET PAGE</h1>
+  </div>
+</template>
+
+<script>
+export default {
+  middleware: 'auth'
+}
+</script>

+ 7 - 0
plugins/README.md

@@ -0,0 +1,7 @@
+# PLUGINS
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains Javascript plugins that you want to run before mounting the root Vue.js application.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).

+ 9 - 0
plugins/filters.js

@@ -0,0 +1,9 @@
+import Vue from "vue"
+
+Vue.filter('shortenText',function(text, maxLength = 300){
+  if(text && typeof text === 'string'){
+    const finalChar = text.length > maxLength ? '...' : ''
+    return text.substr(0, maxLength) + finalChar
+  }
+  return ''
+})

+ 4 - 0
plugins/toasted.js

@@ -0,0 +1,4 @@
+import Vue from 'vue'
+import Toasted from 'vue-toasted';
+
+Vue.use(Toasted)

+ 4 - 0
plugins/vuelidate.js

@@ -0,0 +1,4 @@
+import Vue from 'vue'
+import Vuelidate from 'vuelidate'
+
+Vue.use(Vuelidate)

+ 18 - 0
server/controllers/api.js

@@ -0,0 +1,18 @@
+const ProductHero = require('../models/product-hero');
+const passport = require('passport');
+
+exports.getPageData = function (req, res, next) {
+  const data = {}
+  ProductHero.findOne()
+            .sort({createdAt: -1})
+            .populate('product')
+            .exec(function(errors, productHero) {
+
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    data.productHero = productHero;
+    return res.json(data);
+  })
+};

+ 20 - 0
server/controllers/auth.js

@@ -0,0 +1,20 @@
+
+const passport = require('passport')
+
+exports.onlyAuthUser = function (req, res, next) {
+  if (req.isAuthenticated()) {
+    return next()
+  }
+
+  return res.status(401).send({errors: {auth: 'Not Authenticated!'}})
+}
+
+exports.onlyAdmin = function (req, res, next) {
+  const user = req.user;
+
+  if (user && user['role'] === 'admin') {
+    return next()
+  }
+
+  return res.status(401).send({errors: {auth: 'Not Authorized!'}})
+}

+ 173 - 0
server/controllers/blog.js

@@ -0,0 +1,173 @@
+const Blog = require('../models/blog');
+const slugify = require('slugify');
+const request = require('request');
+const AsyncLock = require('async-lock');
+const lock = new AsyncLock();
+
+const MEDIUM_URL = "https://medium.com/@filipjerga/latest?format=json&limit=20";
+
+function parseFilters(queries) {
+  const parsedQueries = {};
+  if (queries.filter) {
+    Object.keys(queries).forEach((qKey) => {
+      if (qKey.includes('filter')) {
+        const pKey = qKey.match(/\[([^)]+)\]/)[1]
+        parsedQueries[pKey] = queries[qKey]
+      }
+    })
+  }
+
+  return parsedQueries
+}
+
+exports.getBlogs = (req, res) => {
+  const pageSize = parseInt(req.query.pageSize) || 0;
+  const pageNum = parseInt(req.query.pageNum) || 1;
+  const skips = pageSize * (pageNum - 1);
+  const filters = req.query.filter || {}
+
+  Blog.find({status: 'published', ...filters})
+      .sort({'createdAt': -1})
+      .populate('author -_id -password -products -email -role')
+      .skip(skips)
+      .limit(pageSize)
+      .exec(function(errors, publishedBlogs) {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    Blog.count({})
+      .then(count => {
+        return res.json({blogs: publishedBlogs, count, pageCount: Math.ceil(count / pageSize)});
+      });
+  });
+}
+
+exports.getMediumBlogs = (req, res) => {
+  request.get(MEDIUM_URL, (err, apiRes, body) => {
+    if (!err && apiRes.statusCode === 200) {
+      let i = body.indexOf("{");
+      const data = body.substr(i);
+      res.send(data)
+    } else {
+      res.sendStatus(500).json(err);
+    }
+  });
+}
+
+
+exports.getBlogBySlug = (req, res) => {
+  const slug = req.params.slug;
+
+  Blog.findOne({slug})
+      .populate('author -_id -password -products -email -role')
+      .exec(function(errors, foundBlog) {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(foundBlog);
+  });
+}
+
+exports.getBlogById = (req, res) => {
+  const blogId = req.params.id;
+
+  Blog.findById(blogId, (errors, foundBlog) => {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(foundBlog);
+  });
+}
+
+exports.getUserBlogs = (req, res) => {
+  const user = req.user;
+
+  Blog.find({author: user.id}, function(errors, userBlogs) {
+    if (errors) {
+     return res.status(422).send(errors);
+    }
+
+    return res.json(userBlogs);
+  });
+}
+
+exports.updateBlog = (req, res) => {
+  const blogId = req.params.id;
+  const blogData = req.body;
+
+  Blog.findById(blogId, function(errors, foundBlog) {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    if (blogData.status && blogData.status === 'published' && !foundBlog.slug) {
+
+      foundBlog.slug = slugify(foundBlog.title, {
+                                  replacement: '-',    // replace spaces with replacement
+                                  remove: null,        // regex to remove characters
+                                  lower: true          // result in lower case
+                                });
+
+      }
+
+      foundBlog.set(blogData);
+      foundBlog.updatedAt = new Date();
+      foundBlog.save(function(errors, foundBlog) {
+      if (errors) {
+        return res.status(422).send(errors);
+      }
+
+      return res.json(foundBlog);
+    });
+  });
+}
+
+
+exports.createBlog = (req, res) => {
+  const lockId = req.query.lockId;
+
+  if (!lock.isBusy(lockId)) {
+    lock.acquire(lockId, function(done) {
+    const blogData = req.body;
+    const blog = new Blog(blogData);
+    blog.author = req.user;
+
+    blog.save((errors, createdBlog) => {
+      setTimeout(() => done(), 5000);
+
+      if (errors) {
+        return res.status(422).send(errors);
+      }
+
+      return res.json(createdBlog);
+    });
+    }, function(errors, ret) {
+        errors && console.error(errors)
+    });
+  } else {
+    return res.status(422).send({message: 'Blog is getting saved!'});
+  }
+}
+
+
+exports.deleteBlog = (req, res) => {
+  const blogId = req.params.id;
+
+  Blog.deleteOne({_id: blogId}, function(errors) {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    res.json({status: 'deleted'});
+  });
+}
+
+
+
+
+
+
+

+ 13 - 0
server/controllers/category.js

@@ -0,0 +1,13 @@
+const Category = require('../models/category');
+
+exports.getCategories = function(req, res) {
+  Category.find({})
+        .exec((errors, categories) => {
+
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(categories);
+  });
+}

+ 58 - 0
server/controllers/product-hero.js

@@ -0,0 +1,58 @@
+const ProductHero = require('../models/product-hero');
+
+exports.createHero = function (req, res, next) {
+  const productData = req.body;
+
+  const productHero = new ProductHero(productData);
+  productHero.product = productData.product;
+
+  productHero.save((errors, createdHero) => {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(createdHero);
+  });
+};
+
+exports.getProductHeroes = function(req, res, next) {
+
+  ProductHero.find({})
+            .populate('product')
+            .sort({createdAt: -1})
+            .exec(function(errors, heroes) {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(heroes);
+  })
+}
+
+exports.updateProductHeroes = function(req, res, next) {
+  const id = req.params.id;
+
+  ProductHero.findById(id)
+            .populate('product')
+            .exec(function(errors, hero) {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    hero.set({createdAt: new Date()})
+    hero.save((errors, updatedHero) => {
+      if (errors) {
+        return res.status(422).send(errors);
+      }
+
+      return res.json(updatedHero);
+      })
+    })
+  }
+
+
+
+
+
+
+

+ 112 - 0
server/controllers/product.js

@@ -0,0 +1,112 @@
+const Product = require('../models/product');
+const slugify = require('slugify');
+
+exports.getProducts = function (req, res) {
+  Product
+    .find({status: 'published'})
+    .populate('author -_id -password -products -email -role')
+    .populate('category')
+    .sort({'updatedAt': -1})
+    .exec((errors, products) => {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(products);
+    })
+}
+
+exports.getInstructorProducts = function (req, res) {
+  const userId = req.user.id;
+
+  Product
+    .find({author: userId})
+    .populate('author')
+    .sort({'updatedAt': -1})
+    .exec((errors, products) => {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(products);
+    })
+}
+
+exports.getProductById = (req, res) => {
+  const id = req.params.id;
+
+  Product
+    .findById(id)
+    .populate('category')
+    .exec((errors, product) => {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(product);
+    })
+}
+
+exports.getProductBySlug = (req, res) => {
+  const slug = req.params.slug;
+
+  Product
+    .findOne({slug})
+    .populate('author -_id -password -products -email -role')
+    .exec((errors, product) => {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(product);
+    })
+}
+
+// Needs recheck
+exports.createProduct = function (req, res) {
+  const productData = req.body;
+  const user = req.user;
+  const product = new Product(productData);
+  product.author = user;
+
+  product.save((errors, createdProduct) => {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    return res.json(createdProduct);
+  });
+};
+
+exports.updateProduct = function (req, res) {
+  const productId = req.params.id;
+  const productData = req.body;
+
+  Product.findById(productId)
+        .populate('category')
+        .exec((errors, product) => {
+    if (errors) {
+      return res.status(422).send(errors);
+    }
+
+    if (productData.status && productData.status === 'published' && !product.slug) {
+    product.slug = slugify(product.title, {
+                                replacement: '-',    // replace spaces with replacement
+                                remove: null,        // regex to remove characters
+                                lower: true          // result in lower case
+                              });
+    }
+
+    product.set(productData);
+    product.save((errors, savedProduct) => {
+      if (errors) {
+        return res.status(422).send(errors);
+      }
+
+      return res.json(savedProduct);
+    });
+  })
+}
+
+
+

+ 100 - 0
server/controllers/user.js

@@ -0,0 +1,100 @@
+const User = require('../models/user');
+const passport = require('passport');
+
+exports.getCurrentUser = function (req, res, next) {
+  const user = req.user;
+
+  if(!user) {
+    return res.sendStatus(422);
+  }
+
+  return res.json(user);
+};
+
+exports.register = function(req, res) {
+  const registerData = req.body
+
+  if (!registerData.email) {
+    return res.status(422).json({
+      errors: {
+        email: 'is required',
+        message: 'Email is required'
+      }
+    })
+  }
+
+  if (!registerData.password) {
+    return res.status(422).json({
+      errors: {
+        password: 'is required',
+        message: 'Password is required'
+      }
+    })
+  }
+
+  if (registerData.password !== registerData.passwordConfirmation) {
+    return res.status(422).json({
+      errors: {
+        password: 'is not the same as confirmation password',
+        message: 'Password is not the same as confirmation password'
+      }
+    })
+  }
+
+  const user = new User(registerData);
+
+  return user.save((errors, savedUser) => {
+    if (errors) {
+      return res.status(422).json({errors})
+    }
+
+    return res.json(savedUser)
+  })
+}
+
+exports.login = function (req, res, next) {
+  const { email, password } = req.body
+
+  if (!email) {
+    return res.status(422).json({
+      errors: {
+        email: 'is required',
+        message: 'Email is required'
+      }
+    })
+  }
+
+  if (!password) {
+    return res.status(422).json({
+      errors: {
+        password: 'is required',
+        message: 'Password is required'
+      }
+    })
+  }
+
+  return passport.authenticate('local', (err, passportUser) => {
+    if (err) {
+      return next(err)
+    }
+
+    if (passportUser) {
+      req.login(passportUser, function (err) {
+        if (err) { next(err); }
+
+        return res.json(passportUser)
+      });
+    } else {
+      return res.status(422).send({errors: {
+        'message': 'Invalid password or email'
+      }})
+    }
+
+  })(req, res, next)
+}
+
+exports.logout = function (req, res) {
+  req.logout()
+  return res.json({status: 'Session destroyed!'})
+}
+

+ 29 - 0
server/db/index.js

@@ -0,0 +1,29 @@
+const mongoose = require('mongoose');
+const keys = require('../keys');
+const session = require('express-session');
+const MongoDBStore = require('connect-mongodb-session')(session);
+
+require("../models/user");
+require("../models/product");
+require("../models/category");
+require("../models/product-hero");
+require("../models/blog");
+
+exports.initSessionStore = function() {
+  const store = new MongoDBStore({
+    uri: keys.DB_URI,
+    collection: 'eincodeSessions'
+  })
+
+  store.on('error', (error) => console.log(error))
+
+  return store;
+}
+
+exports.connect = function() {
+  return mongoose.connect(keys.DB_URI, { useNewUrlParser: true })
+    .then(() => console.log('DB Connected!'))
+    .catch(err => console.log(err));
+}
+
+

+ 112 - 0
server/dbMocks/data.js

@@ -0,0 +1,112 @@
+const moment = require('moment');
+const mongoose = require('mongoose');
+const User = require('../models/user');
+const Product = require('../models/product');
+const Category = require('../models/category');
+
+const user1Id = mongoose.Types.ObjectId();
+const user2Id = mongoose.Types.ObjectId();
+const user3Id = mongoose.Types.ObjectId();
+
+const product1Id = mongoose.Types.ObjectId();
+
+const category1Id = mongoose.Types.ObjectId();
+const category2Id = mongoose.Types.ObjectId();
+
+module.exports = {
+  "users": {
+    model: User,
+    items: [
+      {
+      "_id": user1Id,
+      "avatar": "https://b.kisscc0.com/20180718/urw/kisscc0-ninja-computer-icons-samurai-youtube-avatar-ninja-5b4ed903c2dd20.4931332915318940197982.jpg",
+      "email": "filip@gmail.com",
+      "name": "Filip Jerga",
+      "info": "Bla bla bla bla",
+      "createdAt": moment().toISOString(),
+      "updatedAt": moment().toISOString(),
+      "username": "Rhonyn99",
+      "password": "testtest",
+      "role": 'admin',
+      "products": [product1Id]
+    },
+    {
+      "_id": user2Id,
+      "avatar": "https://www.clipartmax.com/png/middle/195-1956720_%5B-img%5D-avatar.png",
+      "email": "peter@gmail.com",
+      "name": "Peter Green",
+      "info": "Bla bla bla bla",
+      "createdAt": moment().toISOString(),
+      "updatedAt": moment().toISOString(),
+      "username": "Petergreen",
+      "password": "testtest1"
+    },
+    {
+      "_id": user3Id,
+      "avatar": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQuqyc3j2s3bL4DIkC8uC9h0rcAdsDXcwJPNh8XHWbLQfHbOpVU",
+      "email": "kevin@gmail.com",
+      "name": "Kevin Rock",
+      "info": "I have a famous name",
+      "createdAt": moment().toISOString(),
+      "updatedAt": moment().toISOString(),
+      "username": "Kevin21",
+      "password": "testtest2"
+    }]
+  },
+  categories: {
+    model: Category,
+    items: [
+      {
+        "_id": category1Id,
+        "name": "Web Development"
+      },{
+        "_id": category2Id,
+        "name": "Mobile Development"
+      }
+    ]
+  },
+  products: {
+    model: Product,
+    items: [
+      {
+        "_id": product1Id,
+        "slug": 'Dart-and-Flutter-From-Zero-to-Hero-Practical-Dev-Bootcamp',
+        "title": "Dart and Flutter From Zero to Hero - Practical Dev Bootcamp",
+        "subtitle": "Build real mobile Application for Android and iOS. Learn Dart Framework and discover amazing features of Flutter.",
+        "image": "https://i.udemycdn.com/course/750x422/2381802_d90c_3.jpg",
+        "description": "After dart introduction, we will start learning Flutter Framework. I will explain the basics of flutter, what widgets are, why you need widgets and how they work. We will start with simple examples which will later grow into complex components of our application. I will introduce different architectural patterns on how to manage a state of our application. We will always follow best practices introduced by the Google Flutter team.",
+        "wsl": [
+          {
+            "value": "Learn and master Dart Programming Language"
+          },
+          {
+            "value": "Completely understand the processes and concepts of Flutter Framework"
+          },
+          {
+            "value": "Use gained knowledge to create your own Mobile Applications"
+          },
+          {
+            "value": "Become fluent in concepts and tools like BLoC's, State Management, Services, Widgets and More!"
+          }
+        ],
+        "requirements": [
+          {
+            "value": "No Dart and Flutter previous knowledge is required!"
+          },
+          {
+            "value": "Able to run Android Studio or Xcode Simulator"
+          }
+        ],
+        "promoVideoLink": "https://a2.udemycdn.com/2019-06-16_01-03-38-b4b3369ea5ef3ab87a5c44952d66fbda/WebHD_720p.mp4?nva=20190620043055&token=0d53de33a501d22e72d76",
+        "productLink": "https://www.udemy.com/dart-and-flutter-from-zero-to-hero-practical-dev-bootcamp",
+        "price": 179.99,
+        "discountedPrice": 9.99,
+        "status": "published",
+        "createdAt": moment().toISOString(),
+        "updatedAt": moment().toISOString(),
+        "category": category2Id,
+        "author": user1Id
+      }
+    ]
+  }
+};

+ 47 - 0
server/dbMocks/seedDb.js

@@ -0,0 +1,47 @@
+const mongoose = require('mongoose');
+const User = require('../models/user');
+const data = require('./data.js');
+const keys = require('../keys');
+
+class DB {
+  constructor() {
+    this.collections = Object.keys(data).map(collection => collection);
+  }
+
+  async cleanDb() {
+     for (let collection of this.collections) {
+      var model = data[collection].model;
+      if (model) {
+        await model.deleteMany({}, () => {});
+        console.log(`Data for model ${model.collection.collectionName} Deleted!`)
+      }
+    }
+  }
+
+  async pushDataToDb() {
+    var collectionToResolve = [];
+    for (let collection of this.collections) {
+      collectionToResolve.push(Promise.all(
+        data[collection].items.map(item =>
+         (new data[collection].model(item)).save()
+        )
+      ).then(data => console.log(collection, 'collection saved!')))
+    }
+
+    return Promise.all(collectionToResolve);
+  }
+
+  async seedDb() {
+    await this.cleanDb();
+    await this.pushDataToDb();
+    console.log('Data Populated!')
+  }
+}
+
+mongoose.connect(keys.DB_URI, { useNewUrlParser: true })
+  .then(async () => {
+    const db = new DB();
+    await db.seedDb();
+    console.log('You can close connection now!')
+  })
+  .catch(err => console.log(err));

+ 33 - 0
server/index.js

@@ -0,0 +1,33 @@
+const express = require('express')
+const consola = require('consola')
+const { Nuxt, Builder } = require('nuxt')
+const app = express()
+const keys = require('./keys');
+
+const config = require('../nuxt.config.js')
+config.dev = !(process.env.NODE_ENV === 'production')
+
+async function start() {
+  // Init Nuxt.js
+  const nuxt = new Nuxt(config)
+  const { host, port } = nuxt.options.server
+
+  // Build only in dev mode
+  if (config.dev) {
+    const builder = new Builder(nuxt)
+    await builder.build()
+  } else {
+    await nuxt.ready()
+  }
+
+  // Give nuxt middleware to express
+  app.use(nuxt.render)
+
+  // Listen the server
+  app.listen(port, host)
+  consola.ready({
+    message: `Server listening on http://${host}:${port}`,
+    badge: true
+  })
+}
+start()

+ 5 - 0
server/keys/index.js

@@ -0,0 +1,5 @@
+if (process.env.NODE_ENV === 'production') {
+  module.exports = require('./prod')
+} else {
+  module.exports = require('./dev')
+}

+ 4 - 0
server/keys/prod.js

@@ -0,0 +1,4 @@
+module.exports = {
+  DB_URI: process.env.DB_URI,
+  SESSION_SECRET: process.env.SESSION_SECRET
+}

+ 20 - 0
server/models/blog.js

@@ -0,0 +1,20 @@
+const mongoose = require('mongoose');
+const Schema = mongoose.Schema;
+
+const blogSchema = new Schema({
+  slug: { type: String, unique: true, sparse: true },
+  title: { type: String, maxlength: 96},
+  subtitle: { type: String},
+  content: { type: String, required: true},
+  createdAt: { type: Date, default: Date.now },
+  updatedAt: { type: Date, default: Date.now },
+  featured: { type: Boolean, default: false},
+  status: {
+    type: String,
+    enum: ['active', 'inactive', 'deleted', 'published'],
+    default: 'active'
+  },
+  author: { type: Schema.Types.ObjectId, ref: 'User' }
+});
+
+module.exports = mongoose.model('Blog', blogSchema);

+ 9 - 0
server/models/category.js

@@ -0,0 +1,9 @@
+const mongoose = require('mongoose');
+const Schema = mongoose.Schema;
+
+const categorySchema = new Schema({
+  name: String,
+  createdAt: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('Category', categorySchema);

+ 14 - 0
server/models/product-hero.js

@@ -0,0 +1,14 @@
+const mongoose = require('mongoose');
+const Schema = mongoose.Schema;
+
+const productHeroSchema = new Schema({
+  product: { type: Schema.Types.ObjectId, ref: 'Product' },
+  image: String,
+  title: String,
+  subtitle: String,
+  createdAt: { type: Date, default: Date.now }
+});
+
+const ProductHeroModel = mongoose.model('ProductHero', productHeroSchema );
+
+module.exports = ProductHeroModel

+ 29 - 0
server/models/product.js

@@ -0,0 +1,29 @@
+const mongoose = require('mongoose');
+const Schema = mongoose.Schema;
+
+const productSchema = new Schema({
+  slug: { type: String, unique: true, sparse: true },
+  title: { type: String, required: true },
+  subtitle: String,
+  image: String,
+  description: String,
+  rating: Number,
+  // what students learn
+  wsl: [{type: Schema.Types.Mixed, value: String}],
+  requirements: [{type: Schema.Types.Mixed, value: String}],
+  promoVideoLink: String,
+  productLink: String,
+  price: Number,
+  discountedPrice: Number,
+  status: {
+    type: String,
+    enum: ['active', 'inactive', 'deleted', 'published'],
+    default: 'active'
+  },
+  createdAt: { type: Date, default: Date.now },
+  updatedAt: { type: Date, default: Date.now },
+  category: { type: Schema.Types.ObjectId, ref: 'Category' },
+  author: { type: Schema.Types.ObjectId, ref: 'User' }
+});
+
+module.exports = mongoose.model('Product', productSchema );

+ 61 - 0
server/models/user.js

@@ -0,0 +1,61 @@
+const mongoose = require('mongoose');
+const jwt = require('jsonwebtoken')
+const bcrypt = require('bcrypt')
+const Schema = mongoose.Schema;
+
+const userSchema = new Schema({
+  avatar: String,
+  email: { type: String,
+           required: 'Email is Required',
+           lowercase: true,
+           unique: true,
+           match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/]},
+  name: { type: String,
+          required: true,
+          minlength: [6, 'Too short, min is 6 characters']},
+  username: { type: String,
+          required: true,
+          minlength: [6, 'Too short, min is 6 characters']},
+  password: {
+    type: String,
+    minlength: [4, 'Too short, min is 4 characters'],
+    maxlength: [32, 'Too long, max is 32 characters'],
+    required: 'Password is required'
+  },
+  // Very simplified you should have separate collection with roles
+  // You can create also array of roles in case of multiple roles
+  role: {
+    enum: ['guest', 'admin'],
+    type: String, required: true, default: 'guest'
+  },
+  info: String,
+  products: [{ type: Schema.Types.ObjectId, ref: 'Product' }],
+  createdAt: { type: Date, default: Date.now },
+  updatedAt: { type: Date, default: Date.now }
+});
+
+userSchema.pre("save", function(next){
+   const user = this;
+
+   bcrypt.genSalt(10, function(err, salt) {
+      if(err){ return next(err);}
+
+      bcrypt.hash(user.password, salt, function(err, hash){
+          if(err){ return next(err);}
+
+          user.password = hash;
+          next();
+      });
+   });
+});
+
+//Every user have acces to this methods
+userSchema.methods.comparePassword = function(candidatePassword, callback){
+   bcrypt.compare(candidatePassword, this.password, function(err, isMatch){
+      if(err) {return callback(err);}
+
+      callback(null, isMatch);
+   });
+}
+
+module.exports = mongoose.model('User', userSchema );

+ 8 - 0
server/routes/api.js

@@ -0,0 +1,8 @@
+const express = require('express');
+const router = express.Router();
+
+const ApiCtrl = require('../controllers/api');
+
+router.get('', ApiCtrl.getPageData);
+
+module.exports = router;

+ 32 - 0
server/routes/blog.js

@@ -0,0 +1,32 @@
+const express = require('express');
+const router = express.Router();
+
+const blogCtrl = require('../controllers/blog');
+const AuthCtrl = require('../controllers/auth');
+
+router.get('', blogCtrl.getBlogs);
+
+router.get('/medium', blogCtrl.getMediumBlogs);
+
+router.get('/me', AuthCtrl.onlyAuthUser,
+                  AuthCtrl.onlyAdmin,
+                  blogCtrl.getUserBlogs);
+
+router.get('/:id', blogCtrl.getBlogById);
+
+router.get('/s/:slug', blogCtrl.getBlogBySlug);
+
+router.post('', AuthCtrl.onlyAuthUser,
+                AuthCtrl.onlyAdmin,
+                blogCtrl.createBlog);
+
+router.patch('/:id', AuthCtrl.onlyAuthUser,
+                     AuthCtrl.onlyAdmin,
+                     blogCtrl.updateBlog);
+
+router.delete('/:id', AuthCtrl.onlyAuthUser,
+                      AuthCtrl.onlyAdmin,
+                      blogCtrl.deleteBlog);
+
+module.exports = router;
+

+ 8 - 0
server/routes/category.js

@@ -0,0 +1,8 @@
+const express = require('express');
+const router = express.Router();
+
+const CategoriesCtrl = require('../controllers/category');
+
+router.get('', CategoriesCtrl.getCategories);
+
+module.exports = router;

+ 63 - 0
server/routes/index.js

@@ -0,0 +1,63 @@
+
+const express = require('express')
+const app = express()
+const session = require('express-session');
+const db = require('../db');
+
+const bodyParser = require('body-parser');
+const keys = require('../keys');
+const passport = require('passport');
+
+const usersRoutes = require('./user');
+const productRoutes = require('./product');
+const categoryRoutes = require('./category');
+const blogRoutes = require('./blog');
+const apiRoutes = require('./api');
+const productHeroRoutes = require('./product-hero');
+
+require("../services/passport");
+
+// connect to DB
+db.connect();
+const store = db.initSessionStore();
+
+app.use(bodyParser.json());
+
+// var csrf = require('csurf');
+// consider using this
+
+const sess =
+  { name: 'promo-secure-session',
+    secret: keys.SESSION_SECRET,
+    cookie: { maxAge: 2 * 60 * 60 * 1000 },
+    resave: false,
+    saveUninitialized: false,
+    store
+  }
+
+if (process.env.NODE_ENV === 'production') {
+  app.set('trust proxy', 1);
+  sess.cookie.secure = true;
+  sess.cookie.httpOnly = true;
+  sess.cookie.sameSite = true;
+  sess.cookie.domain = process.env.DOMAIN // .yourdomain.com
+}
+
+app.use(session(sess));
+app.use(passport.initialize());
+app.use(passport.session());
+
+app.use('', apiRoutes);
+app.use('/product-heroes', productHeroRoutes);
+app.use('/users', usersRoutes);
+app.use('/products', productRoutes);
+app.use('/categories', categoryRoutes);
+app.use('/blogs', blogRoutes);
+
+
+module.exports = {
+  path: '/api/v1',
+  handler: app
+}
+
+

+ 17 - 0
server/routes/product-hero.js

@@ -0,0 +1,17 @@
+const express = require('express');
+const router = express.Router();
+
+const ProductHeroCtrl = require('../controllers/product-hero');
+const AuthCtrl = require('../controllers/auth');
+
+router.post('', ProductHeroCtrl.createHero);
+router.get('', AuthCtrl.onlyAuthUser,
+               AuthCtrl.onlyAdmin,
+               ProductHeroCtrl.getProductHeroes);
+
+router.patch('/:id', AuthCtrl.onlyAuthUser,
+                     AuthCtrl.onlyAdmin,
+                     ProductHeroCtrl.updateProductHeroes);
+
+module.exports = router;
+

+ 25 - 0
server/routes/product.js

@@ -0,0 +1,25 @@
+const express = require('express');
+const router = express.Router();
+
+const AuthCtrl = require('../controllers/auth');
+const ProductCtrl = require('../controllers/product');
+
+router.get('', ProductCtrl.getProducts);
+router.get('/user-products',
+           AuthCtrl.onlyAuthUser,
+           AuthCtrl.onlyAdmin,
+           ProductCtrl.getInstructorProducts);
+router.get('/:id', ProductCtrl.getProductById);
+router.get('/s/:slug', ProductCtrl.getProductBySlug);
+
+router.post('',
+            AuthCtrl.onlyAuthUser,
+            AuthCtrl.onlyAdmin,
+            ProductCtrl.createProduct)
+router.patch('/:id',
+              AuthCtrl.onlyAuthUser,
+              AuthCtrl.onlyAdmin,
+              ProductCtrl.updateProduct)
+
+
+module.exports = router;

+ 8 - 0
server/routes/test.js

@@ -0,0 +1,8 @@
+const express = require('express');
+const app = express();
+
+app.get('/', (req, res) => {
+  res.send('Hello World!')
+});
+
+module.exports = router;

+ 13 - 0
server/routes/user.js

@@ -0,0 +1,13 @@
+const express = require('express');
+const router = express.Router();
+
+const UsersCtrl = require('../controllers/user');
+const AuthCtrl = require('../controllers/auth');
+
+router.get('/me', AuthCtrl.onlyAuthUser, UsersCtrl.getCurrentUser);
+
+router.post('/register', UsersCtrl.register)
+router.post('/login', UsersCtrl.login)
+router.post('/logout', UsersCtrl.logout)
+
+module.exports = router;

+ 45 - 0
server/services/passport.js

@@ -0,0 +1,45 @@
+const passport = require('passport');
+const LocalStrategy = require('passport-local');
+const User = require('../models/user');
+const keys = require('../keys');
+
+// Only For Session Authentication !
+passport.serializeUser(function(user, done) {
+  done(null, user.id)
+})
+
+passport.deserializeUser(function(id, done) {
+  User.findById(id, function(err, user) {
+    done(err, user)
+  })
+})
+
+passport.use(new LocalStrategy({
+  usernameField: 'email',
+  passwordField: 'password'
+}, (email, password, done) => {
+  User.findOne({email}, function(err, user) {
+    if (err) { return done(err); }
+    if (!user) { return done(null, false) }
+
+    user.comparePassword(password, function(err, isMatch) {
+      if (err) { return done(err); }
+      if (!isMatch) { return done(null, false) }
+
+      return done(null, user)
+    })
+  })
+}))
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
static/README.md

@@ -0,0 +1,11 @@
+# STATIC
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your static files.
+Each file inside this directory is mapped to `/`.
+Thus you'd want to delete this README.md before deploying to production.
+
+Example: `/static/robots.txt` is mapped as `/robots.txt`.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static).

BIN
static/favicon.ico


+ 10 - 0
store/README.md

@@ -0,0 +1,10 @@
+# STORE
+
+**This directory is not required, you can delete it if you don't want to use it.**
+
+This directory contains your Vuex Store files.
+Vuex Store option is implemented in the Nuxt.js framework.
+
+Creating a file in this directory automatically activates the option in the framework.
+
+More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).

+ 91 - 0
store/auth.js

@@ -0,0 +1,91 @@
+export const state = () => ({
+  user : null
+})
+
+export const getters = {
+  authUser(state){
+    return state.user || null
+  },
+  
+  isAuthenticated(state){
+    return !!state.user
+  },
+  
+  isAdmin(state){
+    return state.user && state.user.role && state.user.role === 'admin'
+  },
+}
+
+export const actions = {
+  async login({commit, state}, loginData) {
+    try{
+      console.log('auth.js actions login >', loginData);
+      console.log('auth.js actions call $axios.$post');
+      const user = await this.$axios.$post('/api/v1/users/login', loginData)
+      console.log('auth.js actions call setAuthUser');
+      commit('setAuthUser', user)
+      return state.user
+    }catch(error){
+      console.log('login error')
+      // console.error(error)
+      return error
+    }
+  },
+  async register(_, registerData) {
+    try{
+      console.log('auth.js register call await this.$axios.$post(/api/v1/users/register)')
+      const user = await this.$axios.$post('/api/v1/users/register', registerData)
+      console.log('auth.js register done await this.$axios.$post(/api/v1/users/register)', user)
+      return user
+    }catch(error){
+      console.log('auth.js register error await this.$axios.$post(/api/v1/users/register)')
+      // let errorMessage = 'Uuups, something went wrong, try to register again!'
+      // if(error.response.data.errors) {
+      //   errorMessage = error.response.data.errors.message
+      // }
+      return error
+    }
+  },
+  async logout({commit}) {
+    console.log('auth.js logout call await this.$axios.$post(/api/v1/users/logout)')
+    try{
+      const result = await this.$axios.$post('/api/v1/users/logout')
+      console.log('auth.js logout done await this.$axios.$post(/api/v1/users/logout)', result)
+    }catch(error){
+      console.log('auth.js logout error', error)
+      return error
+    }
+
+    commit('setAuthUser', null)
+    return true
+
+  },
+  async getAuthUser({commit, getters, state}) {
+    const authUser = getters.authUser
+
+    if (authUser) {
+      console.log('auth.js actions getAuthUser authUser ', authUser) 
+      return authUser 
+    }
+
+    try {
+      console.log('auth.js getAuthUser call await this.$axios.$get(/api/v1/users/me)')
+      const user = await this.$axios.$get('/api/v1/users/me')
+      console.log('auth.js getAuthUser done await this.$axios.$get(/api/v1/users/me) : ', user)
+      commit('setAuthUser', user)
+      return state.user
+    }catch(error){
+      console.log('auth.js getAuthUser error await this.$axios.$get(/api/v1/users/me) : ')
+      commit('setAuthUser', null)
+      return error
+    }
+  }
+}
+
+
+export const mutations = {
+  setAuthUser(state, user) {
+    state.user = user
+    console.log('auth.js actions done setAuthUser');
+  }
+}

+ 34 - 0
store/category.js

@@ -0,0 +1,34 @@
+export const state = () => ({
+  items: []
+})
+
+export const getters = {
+  hasCategories(state) {
+    return state.items.length > 0
+  }
+}
+
+export const actions = {
+  async fetchCategories({state, commit, getters}) {
+    if (getters.hasCategories) return
+
+    try{
+      console.log('category.js action call fetchCategories')
+      const categories = await this.$axios.$get('/api/v1/categories')
+      console.log('category.js action done fetchCategories', categories)
+      console.log('category.js mutations call setCategories')
+      commit('setCategories', categories)
+      console.log('category.js mutations done setCategories')
+      return state.items
+
+    }catch(error){
+      return error
+    }
+  }
+}
+
+export const mutations = {
+  setCategories(state, categories) {
+    state.items = categories
+  }
+}

+ 14 - 0
store/course.js

@@ -0,0 +1,14 @@
+export const state = () => ({
+  items : []
+})
+
+export const actions = {
+  async fetchCourses({commit}) {
+    console.log('store/course.js actions > fetchCourses')
+    const courses = await this.$axios.$get('/api/v1/products')
+    console.log('store/course.js actions > fetchCourses > await this.$axios.$get')
+    commit('setItems', {resource: 'course', items: courses}, {root: true})
+    console.log('store/course.js actions > fetchCourses > await this.$axios.$get > commit')
+    return state.items
+  }
+}

+ 14 - 0
store/index.js

@@ -0,0 +1,14 @@
+export const mutations = {
+  setItems(state, {resource, items}) {
+    console.log('store/index.js mutations > setItems')
+    state[resource].items = items
+  }
+}
+
+export const actions = {
+  async nuxtServerInit({commit, dispatch}) {
+    console.log('index.js actions call await dispatch(auth/getAuthUser)')
+    await dispatch('auth/getAuthUser').catch(() => console.log('Not Authenticated!'))
+    console.log('index.js actions done await dispatch(auth/getAuthUser)')
+  }
+}

+ 62 - 0
store/instructor/course.js

@@ -0,0 +1,62 @@
+
+export const state = () => ({
+  items : [],
+  item : []
+})
+
+export const actions = {
+  async fetchInstructorCourses({commit}) {
+    try{ 
+      console.log('courses.js actions call fetchInstructorCourses')
+      const courses = await this.$axios.$get('/api/v1/products/user-products')
+      console.log('courses.js actions done fetchInstructorCourses')
+      console.log('courses.js mutations call setCourses')
+      commit('setCourses', courses)
+      console.log('courses.js mutations done setCourses')
+      return courses
+    }catch(error){
+      return error
+    }
+  },
+  async createCourse(_, courseData) {
+    try {
+      console.log('instructor/course.js actions createCourse call axios.$post->products')
+      const result = await this.$axios.$post('/api/v1/products', courseData)
+      console.log('instructor/course.js actions createCourse done axios.$post->products')
+    } catch (error) {
+      return error
+    }
+  },
+  async fetchCourseById({commit, state}, courseId){
+    try {
+      console.log('instructor/course.js actions fetchCourseById call axios.$get->courseId')
+      const course = await this.$axios.$get(`/api/v1/products/${courseId}`)
+      console.log('instructor/course.js actions fetchCourseById done axios.$get->courseId')
+      console.log('courses.js mutations call setCourse')
+      commit('setCourse', course)
+      console.log('courses.js mutations done setCourse')
+      return state.item
+    } catch (error) {
+      return error
+    }
+  },
+}
+
+export const mutations = {
+  setCourses(state, courses) {
+    state.items = courses
+  },
+  setCourse(state, course) {
+    state.item = course
+  },
+  addLine(state, field) {
+    console.log('course.js mutations call addLine params > ', field)
+    state.item[field].push({value: ''})
+    console.log('course.js mutations done addLine params')
+  },
+  removeLine(state, {field, index}) {
+    console.log('course.js mutations call removeLine params > ', field, ':', index)
+    state.item[field].splice(index, 1)
+    console.log('course.js mutations done removeLine params')
+  }
+}