├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── netlify.toml ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── css │ │ └── tailwind.css ├── components │ ├── AuthUser.vue │ ├── AuthUserForm.vue │ ├── BaseBtn.vue │ ├── BaseInput.vue │ ├── BasePagination.vue │ ├── FileUpload.vue │ ├── FlashMessage.vue │ ├── Header.vue │ ├── LoginForm.vue │ ├── Logout.vue │ ├── MessageForm.vue │ ├── Messages.vue │ ├── RegisterForm.vue │ ├── UpdatePassword.vue │ ├── VerifyEmail.vue │ └── icons │ │ ├── AvatarIcon.vue │ │ ├── HomeIcon.vue │ │ ├── LoginIcon.vue │ │ ├── LogoutIcon.vue │ │ └── MailIcon.vue ├── main.js ├── middleware │ ├── admin.js │ ├── auth.js │ └── guest.js ├── router │ ├── index.js │ └── middlewarePipeline.js ├── services │ ├── API.js │ ├── AuthService.js │ ├── FileService.js │ ├── MessageService.js │ └── UserService.js ├── store │ ├── index.js │ └── modules │ │ ├── Auth.js │ │ ├── Message.js │ │ └── User.js ├── utils │ └── helpers.js └── views │ ├── Dashboard.vue │ ├── ForgotPassword.vue │ ├── Home.vue │ ├── Login.vue │ ├── NotFound.vue │ ├── Register.vue │ ├── ResetPassword.vue │ ├── User.vue │ └── Users.vue └── tailwind.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gareth Redfern 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Vue SPA Using Sanctum & Fortify Authentication 2 | 3 | ## Documentation 4 | 5 | The full documentation for this demo app can be found at [Build a Laravel Vue Spa](https://laravelvuespa.com/). 6 | 7 | ## Project setup 8 | ``` 9 | npm install 10 | ``` 11 | 12 | **IMPORTANT** make sure you have a .env.local file with your API URL added 13 | ``` 14 | VUE_APP_API_URL=http://localhost 15 | ``` 16 | 17 | ### Compiles and hot-reloads for development 18 | ``` 19 | npm run serve 20 | ``` 21 | 22 | ### Compiles and minifies for production 23 | ``` 24 | npm run build 25 | ``` 26 | 27 | ### Lints and fixes files 28 | ``` 29 | npm run lint 30 | ``` 31 | 32 | ### Customize configuration 33 | See [Configuration Reference](https://cli.vuejs.org/config/). 34 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "npm run build" 4 | [[redirects]] 5 | from = "/*" 6 | to = "/index.html" 7 | status = 200 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-vue", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "core-js": "^3.15.2", 13 | "vue": "^2.6.14", 14 | "vue-router": "^3.5.2", 15 | "vuex": "^3.6.2" 16 | }, 17 | "devDependencies": { 18 | "@tailwindcss/forms": "^0.2.1", 19 | "@tailwindcss/postcss7-compat": "^2.2.4", 20 | "@vue/cli-plugin-babel": "^4.5.10", 21 | "@vue/cli-plugin-eslint": "^4.5.10", 22 | "@vue/cli-plugin-router": "^4.5.10", 23 | "@vue/cli-plugin-vuex": "^4.5.10", 24 | "@vue/cli-service": "^4.5.10", 25 | "@vue/eslint-config-prettier": "^6.0.0", 26 | "autoprefixer": "^9.8.6", 27 | "babel-eslint": "^10.1.0", 28 | "eslint": "^7.18.0", 29 | "eslint-plugin-prettier": "^3.4.0", 30 | "eslint-plugin-vue": "^7.13.0", 31 | "postcss": "^7.0.36", 32 | "prettier": "^2.3.2", 33 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2", 34 | "vue-template-compiler": "^2.6.14" 35 | }, 36 | "prettier": { 37 | "trailingComma": "es5", 38 | "tabWidth": 2, 39 | "semi": true, 40 | "singleQuote": false 41 | }, 42 | "eslintConfig": { 43 | "root": true, 44 | "env": { 45 | "node": true 46 | }, 47 | "extends": [ 48 | "plugin:vue/essential", 49 | "eslint:recommended", 50 | "@vue/prettier" 51 | ], 52 | "parserOptions": { 53 | "parser": "babel-eslint" 54 | }, 55 | "rules": {} 56 | }, 57 | "browserslist": [ 58 | "> 1%", 59 | "last 2 versions", 60 | "not dead" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garethredfern/laravel-vue/16d56b9c79a9d761d65251ef0d1927d24d45b75d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 33 | -------------------------------------------------------------------------------- /src/assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply bg-gray-50; 8 | } 9 | .base-link { 10 | @apply text-blue-400; 11 | @apply hover:text-blue-500; 12 | @apply hover:underline; 13 | @apply transition; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/AuthUser.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | -------------------------------------------------------------------------------- /src/components/AuthUserForm.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 70 | -------------------------------------------------------------------------------- /src/components/BaseBtn.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /src/components/BaseInput.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 68 | -------------------------------------------------------------------------------- /src/components/BasePagination.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 116 | -------------------------------------------------------------------------------- /src/components/FileUpload.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 73 | -------------------------------------------------------------------------------- /src/components/FlashMessage.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 68 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | -------------------------------------------------------------------------------- /src/components/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 77 | -------------------------------------------------------------------------------- /src/components/Logout.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /src/components/MessageForm.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 52 | -------------------------------------------------------------------------------- /src/components/Messages.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 66 | -------------------------------------------------------------------------------- /src/components/RegisterForm.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 77 | -------------------------------------------------------------------------------- /src/components/UpdatePassword.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 76 | -------------------------------------------------------------------------------- /src/components/VerifyEmail.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 46 | -------------------------------------------------------------------------------- /src/components/icons/AvatarIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/components/icons/HomeIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/components/icons/LoginIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /src/components/icons/LogoutIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /src/components/icons/MailIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import "@/assets/css/tailwind.css"; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | router, 11 | store, 12 | render: (h) => h(App), 13 | }).$mount("#app"); 14 | -------------------------------------------------------------------------------- /src/middleware/admin.js: -------------------------------------------------------------------------------- 1 | export default function admin({ next, store }) { 2 | if (store.getters["auth/isAdmin"]) next(); 3 | else next({ name: "notFound" }); 4 | } 5 | -------------------------------------------------------------------------------- /src/middleware/auth.js: -------------------------------------------------------------------------------- 1 | export default function auth({ to, next, store }) { 2 | const loginQuery = { path: "/login", query: { redirect: to.fullPath } }; 3 | 4 | if (!store.getters["auth/authUser"]) { 5 | store.dispatch("auth/getAuthUser").then(() => { 6 | if (!store.getters["auth/authUser"]) next(loginQuery); 7 | else next(); 8 | }); 9 | } else { 10 | next(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/middleware/guest.js: -------------------------------------------------------------------------------- 1 | export default function guest({ next, store }) { 2 | const storageItem = window.localStorage.getItem("guest"); 3 | if (storageItem === "isNotGuest" && !store.getters["auth/authUser"]) { 4 | store.dispatch("auth/getAuthUser").then(() => { 5 | if (store.getters["auth/authUser"]) { 6 | next({ name: "dashboard" }); 7 | } else { 8 | store.dispatch("auth/setGuest", { value: "isGuest" }); 9 | next(); 10 | } 11 | }); 12 | } else { 13 | next(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import store from "@/store/index"; 3 | import VueRouter from "vue-router"; 4 | import auth from "@/middleware/auth"; 5 | import admin from "@/middleware/admin"; 6 | import guest from "@/middleware/guest"; 7 | import middlewarePipeline from "@/router/middlewarePipeline"; 8 | 9 | Vue.use(VueRouter); 10 | 11 | const routes = [ 12 | { 13 | path: "/", 14 | name: "home", 15 | meta: { middleware: [guest] }, 16 | component: () => import(/* webpackChunkName: "home" */ "../views/Home"), 17 | }, 18 | { 19 | path: "/dashboard", 20 | name: "dashboard", 21 | meta: { middleware: [auth] }, 22 | component: () => 23 | import(/* webpackChunkName: "dashboard" */ "../views/Dashboard"), 24 | }, 25 | { 26 | path: "/user", 27 | name: "user", 28 | meta: { middleware: [auth] }, 29 | component: () => import(/* webpackChunkName: "user" */ "../views/User"), 30 | }, 31 | { 32 | path: "/users", 33 | name: "users", 34 | meta: { middleware: [auth, admin] }, 35 | component: () => import(/* webpackChunkName: "users" */ "../views/Users"), 36 | }, 37 | { 38 | path: "/login", 39 | name: "login", 40 | meta: { middleware: [guest] }, 41 | component: () => import(/* webpackChunkName: "login" */ "../views/Login"), 42 | }, 43 | { 44 | path: "/register", 45 | name: "register", 46 | meta: { middleware: [guest] }, 47 | component: () => 48 | import(/* webpackChunkName: "register" */ "../views/Register"), 49 | }, 50 | { 51 | path: "/reset-password", 52 | name: "resetPassword", 53 | meta: { middleware: [guest] }, 54 | component: () => 55 | import(/* webpackChunkName: "reset-password" */ "../views/ResetPassword"), 56 | }, 57 | { 58 | path: "/forgot-password", 59 | name: "forgotPassword", 60 | meta: { middleware: [guest] }, 61 | component: () => 62 | import( 63 | /* webpackChunkName: "forgot-password" */ "../views/ForgotPassword" 64 | ), 65 | }, 66 | { 67 | path: "/:catchAll(.*)", 68 | name: "notFound", 69 | component: () => 70 | import(/* webpackChunkName: "not-found" */ "../views/NotFound"), 71 | }, 72 | ]; 73 | 74 | const router = new VueRouter({ 75 | mode: "history", 76 | routes, 77 | scrollBehavior(to, from, savedPosition) { 78 | if (savedPosition) { 79 | return savedPosition; 80 | } else { 81 | return { x: 0, y: 0 }; 82 | } 83 | }, 84 | }); 85 | 86 | router.beforeEach((to, from, next) => { 87 | const middleware = to.meta.middleware; 88 | const context = { to, from, next, store }; 89 | 90 | if (!middleware) { 91 | return next(); 92 | } 93 | 94 | middleware[0]({ 95 | ...context, 96 | next: middlewarePipeline(context, middleware, 1), 97 | }); 98 | }); 99 | 100 | export default router; 101 | -------------------------------------------------------------------------------- /src/router/middlewarePipeline.js: -------------------------------------------------------------------------------- 1 | export default function middlewarePipeline(context, middleware, index) { 2 | const nextMiddleware = middleware[index]; 3 | if (!nextMiddleware) { 4 | return context.next; 5 | } 6 | return () => { 7 | nextMiddleware({ 8 | ...context, 9 | next: middlewarePipeline(context, middleware, index + 1), 10 | }); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/services/API.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is the initial API interface 3 | * we set the base URL for the API 4 | */ 5 | 6 | import axios from "axios"; 7 | import store from "@/store"; 8 | 9 | export const apiClient = axios.create({ 10 | baseURL: process.env.VUE_APP_API_URL + "/api", 11 | withCredentials: true, // required to handle the CSRF token 12 | }); 13 | 14 | /* 15 | * Add a response interceptor 16 | */ 17 | apiClient.interceptors.response.use( 18 | (response) => { 19 | return response; 20 | }, 21 | function (error) { 22 | if ( 23 | error.response && 24 | [401, 419].includes(error.response.status) && 25 | store.getters["auth/authUser"] && 26 | !store.getters["auth/guest"] 27 | ) { 28 | store.dispatch("auth/logout"); 29 | } 30 | return Promise.reject(error); 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /src/services/AuthService.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import store from "@/store"; 3 | 4 | export const authClient = axios.create({ 5 | baseURL: process.env.VUE_APP_API_URL, 6 | withCredentials: true, // required to handle the CSRF token 7 | }); 8 | 9 | /* 10 | * Add a response interceptor 11 | */ 12 | authClient.interceptors.response.use( 13 | (response) => { 14 | return response; 15 | }, 16 | function (error) { 17 | if ( 18 | error.response && 19 | [401, 419].includes(error.response.status) && 20 | store.getters["auth/authUser"] && 21 | !store.getters["auth/guest"] 22 | ) { 23 | store.dispatch("auth/logout"); 24 | } 25 | return Promise.reject(error); 26 | } 27 | ); 28 | 29 | export default { 30 | async login(payload) { 31 | await authClient.get("/sanctum/csrf-cookie"); 32 | return authClient.post("/login", payload); 33 | }, 34 | logout() { 35 | return authClient.post("/logout"); 36 | }, 37 | async forgotPassword(payload) { 38 | await authClient.get("/sanctum/csrf-cookie"); 39 | return authClient.post("/forgot-password", payload); 40 | }, 41 | getAuthUser() { 42 | return authClient.get("/api/users/auth"); 43 | }, 44 | async resetPassword(payload) { 45 | await authClient.get("/sanctum/csrf-cookie"); 46 | return authClient.post("/reset-password", payload); 47 | }, 48 | updatePassword(payload) { 49 | return authClient.put("/user/password", payload); 50 | }, 51 | async registerUser(payload) { 52 | await authClient.get("/sanctum/csrf-cookie"); 53 | return authClient.post("/register", payload); 54 | }, 55 | sendVerification(payload) { 56 | return authClient.post("/email/verification-notification", payload); 57 | }, 58 | updateUser(payload) { 59 | return authClient.put("/user/profile-information", payload); 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /src/services/FileService.js: -------------------------------------------------------------------------------- 1 | import * as API from "@/services/API"; 2 | 3 | export default { 4 | uploadFile(payload) { 5 | return API.apiClient.post(payload.endpoint, payload.file); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/services/MessageService.js: -------------------------------------------------------------------------------- 1 | import * as API from "@/services/API"; 2 | 3 | export default { 4 | getMessages(page) { 5 | return API.apiClient.get(`/messages/?page=${page}`); 6 | }, 7 | postMessage(payload) { 8 | return API.apiClient.post("/messages", payload); 9 | }, 10 | paginateMessages(link) { 11 | return API.apiClient.get(link); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/services/UserService.js: -------------------------------------------------------------------------------- 1 | import * as API from "@/services/API"; 2 | 3 | export default { 4 | getUser(userId) { 5 | return API.apiClient.get(`/users/${userId}`); 6 | }, 7 | getUsers(page) { 8 | return API.apiClient.get(`/users/?page=${page}`); 9 | }, 10 | paginateUsers(link) { 11 | return API.apiClient.get(link); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | 4 | import * as auth from "@/store/modules/Auth"; 5 | import * as user from "@/store/modules/User"; 6 | import * as message from "@/store/modules/Message"; 7 | 8 | Vue.use(Vuex); 9 | 10 | export default new Vuex.Store({ 11 | strict: true, 12 | 13 | modules: { 14 | auth, 15 | user, 16 | message, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/store/modules/Auth.js: -------------------------------------------------------------------------------- 1 | import router from "@/router"; 2 | import { getError } from "@/utils/helpers"; 3 | import AuthService from "@/services/AuthService"; 4 | 5 | export const namespaced = true; 6 | 7 | export const state = { 8 | user: null, 9 | loading: false, 10 | error: null, 11 | }; 12 | 13 | export const mutations = { 14 | SET_USER(state, user) { 15 | state.user = user; 16 | }, 17 | SET_LOADING(state, loading) { 18 | state.loading = loading; 19 | }, 20 | SET_ERROR(state, error) { 21 | state.error = error; 22 | }, 23 | }; 24 | 25 | export const actions = { 26 | logout({ commit, dispatch }) { 27 | return AuthService.logout() 28 | .then(() => { 29 | commit("SET_USER", null); 30 | dispatch("setGuest", { value: "isGuest" }); 31 | if (router.currentRoute.name !== "login") 32 | router.push({ path: "/login" }); 33 | }) 34 | .catch((error) => { 35 | commit("SET_ERROR", getError(error)); 36 | }); 37 | }, 38 | async getAuthUser({ commit }) { 39 | commit("SET_LOADING", true); 40 | try { 41 | const response = await AuthService.getAuthUser(); 42 | commit("SET_USER", response.data.data); 43 | commit("SET_LOADING", false); 44 | return response.data.data; 45 | } catch (error) { 46 | commit("SET_LOADING", false); 47 | commit("SET_USER", null); 48 | commit("SET_ERROR", getError(error)); 49 | } 50 | }, 51 | setGuest(context, { value }) { 52 | window.localStorage.setItem("guest", value); 53 | }, 54 | }; 55 | 56 | export const getters = { 57 | authUser: (state) => { 58 | return state.user; 59 | }, 60 | isAdmin: (state) => { 61 | return state.user ? state.user.isAdmin : false; 62 | }, 63 | error: (state) => { 64 | return state.error; 65 | }, 66 | loading: (state) => { 67 | return state.loading; 68 | }, 69 | loggedIn: (state) => { 70 | return !!state.user; 71 | }, 72 | guest: () => { 73 | const storageItem = window.localStorage.getItem("guest"); 74 | if (!storageItem) return false; 75 | if (storageItem === "isGuest") return true; 76 | if (storageItem === "isNotGuest") return false; 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /src/store/modules/Message.js: -------------------------------------------------------------------------------- 1 | import { getError } from "@/utils/helpers"; 2 | import MessageService from "@/services/MessageService"; 3 | 4 | export const namespaced = true; 5 | 6 | function setPaginatedMessages(commit, response) { 7 | commit("SET_MESSAGES", response.data.data); 8 | commit("SET_META", response.data.meta); 9 | commit("SET_LINKS", response.data.links); 10 | commit("SET_LOADING", false); 11 | } 12 | 13 | export const state = { 14 | messages: [], 15 | meta: null, 16 | links: null, 17 | loading: false, 18 | error: null, 19 | }; 20 | 21 | export const mutations = { 22 | SET_MESSAGES(state, messages) { 23 | state.messages = messages; 24 | }, 25 | SET_META(state, meta) { 26 | state.meta = meta; 27 | }, 28 | SET_LINKS(state, links) { 29 | state.links = links; 30 | }, 31 | SET_LOADING(state, loading) { 32 | state.loading = loading; 33 | }, 34 | SET_ERROR(state, error) { 35 | state.error = error; 36 | }, 37 | }; 38 | 39 | export const actions = { 40 | getMessages({ commit }, page) { 41 | commit("SET_LOADING", true); 42 | MessageService.getMessages(page) 43 | .then((response) => { 44 | setPaginatedMessages(commit, response); 45 | }) 46 | .catch((error) => { 47 | commit("SET_LOADING", false); 48 | commit("SET_ERROR", getError(error)); 49 | }); 50 | }, 51 | postMessage({ commit }, payload) { 52 | return MessageService.postMessage(payload).then((response) => { 53 | setPaginatedMessages(commit, response); 54 | }); 55 | }, 56 | paginateMessages({ commit }, link) { 57 | commit("SET_LOADING", true); 58 | MessageService.paginateMessages(link) 59 | .then((response) => { 60 | setPaginatedMessages(commit, response); 61 | }) 62 | .catch((error) => { 63 | commit("SET_LOADING", false); 64 | commit("SET_ERROR", getError(error)); 65 | }); 66 | }, 67 | }; 68 | 69 | export const getters = { 70 | messages: (state) => { 71 | return state.messages; 72 | }, 73 | meta: (state) => { 74 | return state.meta; 75 | }, 76 | links: (state) => { 77 | return state.links; 78 | }, 79 | loading: (state) => { 80 | return state.loading; 81 | }, 82 | error: (state) => { 83 | return state.error; 84 | }, 85 | }; 86 | -------------------------------------------------------------------------------- /src/store/modules/User.js: -------------------------------------------------------------------------------- 1 | import { getError } from "@/utils/helpers"; 2 | import UserService from "@/services/UserService"; 3 | 4 | export const namespaced = true; 5 | 6 | function setPaginatedUsers(commit, response) { 7 | commit("SET_USERS", response.data.data); 8 | commit("SET_META", response.data.meta); 9 | commit("SET_LINKS", response.data.links); 10 | commit("SET_LOADING", false); 11 | } 12 | 13 | export const state = { 14 | users: [], 15 | meta: null, 16 | links: null, 17 | loading: false, 18 | error: null, 19 | }; 20 | 21 | export const mutations = { 22 | SET_USERS(state, users) { 23 | state.users = users; 24 | }, 25 | SET_META(state, meta) { 26 | state.meta = meta; 27 | }, 28 | SET_LINKS(state, links) { 29 | state.links = links; 30 | }, 31 | SET_LOADING(state, loading) { 32 | state.loading = loading; 33 | }, 34 | SET_ERROR(state, error) { 35 | state.error = error; 36 | }, 37 | }; 38 | 39 | export const actions = { 40 | getUsers({ commit }, page) { 41 | commit("SET_LOADING", true); 42 | UserService.getUsers(page) 43 | .then((response) => { 44 | setPaginatedUsers(commit, response); 45 | }) 46 | .catch((error) => { 47 | commit("SET_LOADING", false); 48 | commit("SET_ERROR", getError(error)); 49 | }); 50 | }, 51 | paginateUsers({ commit }, link) { 52 | commit("SET_LOADING", true); 53 | UserService.paginateUsers(link) 54 | .then((response) => { 55 | setPaginatedUsers(commit, response); 56 | }) 57 | .catch((error) => { 58 | commit("SET_LOADING", false); 59 | commit("SET_ERROR", getError(error)); 60 | }); 61 | }, 62 | }; 63 | 64 | export const getters = { 65 | users: (state) => { 66 | return state.users; 67 | }, 68 | meta: (state) => { 69 | return state.meta; 70 | }, 71 | links: (state) => { 72 | return state.links; 73 | }, 74 | loading: (state) => { 75 | return state.loading; 76 | }, 77 | error: (state) => { 78 | return state.error; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | export const getError = (error) => { 2 | const errorMessage = "API Error, please try again."; 3 | 4 | if (error.name === "Fetch User") { 5 | return error.message; 6 | } 7 | 8 | if (!error.response) { 9 | console.error(`API ${error.config.url} not found`); 10 | return errorMessage; 11 | } 12 | if (process.env.NODE_ENV === "development") { 13 | console.error(error.response.data); 14 | console.error(error.response.status); 15 | console.error(error.response.headers); 16 | } 17 | if (error.response.data && error.response.data.errors) { 18 | return error.response.data.errors; 19 | } 20 | 21 | return errorMessage; 22 | }; 23 | -------------------------------------------------------------------------------- /src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /src/views/ForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 58 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /src/views/Register.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/views/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 81 | -------------------------------------------------------------------------------- /src/views/User.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /src/views/Users.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 77 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ["./src/**/*.html", "./src/**/*.vue"], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | sans: ["'Open Sans'", "sans-serif"], 8 | }, 9 | }, 10 | }, 11 | variants: { 12 | extend: {}, 13 | }, 14 | plugins: [require("@tailwindcss/forms")], 15 | }; 16 | --------------------------------------------------------------------------------