├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.css └── index.html ├── src ├── .env.js ├── assets │ ├── logo.png │ └── meal.jpeg ├── authorization.js ├── components │ ├── Auth │ │ ├── Login.vue │ │ └── Register.vue │ ├── Food │ │ ├── AllFood.vue │ │ ├── CreateFood.vue │ │ ├── EditFood.vue │ │ └── SingleFood.vue │ ├── Main.vue │ └── Nav │ │ ├── Footer.vue │ │ └── Layout.vue ├── main.js ├── route.js └── store │ └── store.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An article was written about this project [here](https://dev.to/stevensunflash/using-domain-driven-design-ddd-in-golang-3ee5) 2 | 3 | A Backened was also built with Golang and deployed to heroku. 4 | 5 | Get backend github repository [here](https://github.com/victorsteven/food-app-server) 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "food-app-client", 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.19.2", 12 | "core-js": "^3.4.4", 13 | "sweetalert2": "^7.33.1", 14 | "vue": "^2.6.10", 15 | "vue-infinite-scroll": "^2.0.2", 16 | "vue-material": "^1.0.0-beta-11", 17 | "vue-router": "^3.1.5", 18 | "vue-tinymce-editor": "^1.6.2", 19 | "vue2-editor": "^2.10.2", 20 | "vuex": "^3.1.2" 21 | }, 22 | "devDependencies": { 23 | "@vue/cli-plugin-babel": "^4.1.0", 24 | "@vue/cli-plugin-eslint": "^4.1.0", 25 | "@vue/cli-service": "^4.1.0", 26 | "babel-eslint": "^10.0.3", 27 | "eslint": "^5.16.0", 28 | "eslint-plugin-vue": "^5.0.0", 29 | "sass-loader": "^8.0.2", 30 | "vue-template-compiler": "^2.6.10" 31 | }, 32 | "eslintConfig": { 33 | "root": true, 34 | "env": { 35 | "node": true 36 | }, 37 | "extends": [ 38 | "plugin:vue/essential", 39 | "eslint:recommended" 40 | ], 41 | "rules": { 42 | "no-console": "off" 43 | }, 44 | "parserOptions": { 45 | "parser": "babel-eslint" 46 | } 47 | }, 48 | "browserslist": [ 49 | "> 1%", 50 | "last 2 versions" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorsteven/food-app-client/bc02fa2ef07fd8d50c72b09427d2cbeffde72bea/public/favicon.ico -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorsteven/food-app-client/bc02fa2ef07fd8d50c72b09427d2cbeffde72bea/public/index.css -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | food-app-client 15 | 16 | 17 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/.env.js: -------------------------------------------------------------------------------- 1 | let API_ROUTE 2 | 3 | process.env.NODE_ENV == 'development' 4 | ? API_ROUTE = 'http://127.0.0.1:8888' 5 | : API_ROUTE = 'https://food-app-golang.herokuapp.com' 6 | 7 | export default API_ROUTE -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorsteven/food-app-client/bc02fa2ef07fd8d50c72b09427d2cbeffde72bea/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/meal.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorsteven/food-app-client/bc02fa2ef07fd8d50c72b09427d2cbeffde72bea/src/assets/meal.jpeg -------------------------------------------------------------------------------- /src/authorization.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import API_ROUTE from './.env'; 3 | 4 | const customAxios = axios.create({}) 5 | 6 | customAxios.interceptors.request.use( 7 | (config) => { 8 | let token = localStorage.getItem('access_token') 9 | if (token) { 10 | config.headers['Authorization'] = `Bearer ${ token }` 11 | } 12 | return config 13 | }, 14 | (error) => { 15 | return Promise.reject(error) 16 | } 17 | ) 18 | 19 | //Response 20 | customAxios.interceptors.response.use( 21 | (response) => { 22 | //if the request succeeds, we dont have to do anything and just return the response 23 | return response 24 | }, 25 | (error) => { 26 | if(error.response.data.error == "Token is expired" || error.response.data.error == "token contains an invalid number of segments") { 27 | return refreshToken(error) 28 | } 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | async function refreshToken(error) { 34 | console.log("We entered here") 35 | try { 36 | const originalRequest = error.config; 37 | 38 | const res = await axios.post(`${API_ROUTE}/refresh`, { 39 | "refresh_token": localStorage.getItem('refresh_token') 40 | }) 41 | localStorage.clear() 42 | if (!res) { 43 | return Promise.reject(error); 44 | } 45 | if (res.status == 201) { 46 | localStorage.setItem('access_token', res.data.access_token) 47 | localStorage.setItem('refresh_token', res.data.refresh_token) 48 | } 49 | //Change the authorization header 50 | axios.defaults.headers.common['Authorization'] = 'Bearer ' + res.data.access_token; 51 | 52 | //return originalRequest object with Axios 53 | return customAxios(originalRequest); 54 | } catch(err) { 55 | console.log("the ode: ", err.response.data) 56 | //The refresh token has expired, clear the cache and redirect to login 57 | if (err.response.data == "Refresh token has expired" || err.response.data == "Token is expired") { 58 | localStorage.removeItem("access_token") 59 | localStorage.removeItem("refresh_token") 60 | window.location.href = "/login" 61 | } 62 | return Promise.reject(err); 63 | } 64 | } 65 | export default customAxios -------------------------------------------------------------------------------- /src/components/Auth/Login.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | 50 | 84 | 85 | -------------------------------------------------------------------------------- /src/components/Auth/Register.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 61 | 62 | 98 | 99 | -------------------------------------------------------------------------------- /src/components/Food/AllFood.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 | 67 | 68 | -------------------------------------------------------------------------------- /src/components/Food/CreateFood.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 52 | 113 | 114 | -------------------------------------------------------------------------------- /src/components/Food/EditFood.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 131 | 132 | -------------------------------------------------------------------------------- /src/components/Food/SingleFood.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 106 | 107 | 164 | -------------------------------------------------------------------------------- /src/components/Main.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/Nav/Footer.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/Nav/Layout.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 98 | 99 | 136 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Main from './components/Main' 3 | import router from './route' 4 | import VueMaterial from 'vue-material' 5 | import 'vue-material/dist/vue-material.min.css' 6 | 7 | import { store } from './store/store' 8 | Vue.use(VueMaterial) 9 | 10 | 11 | Vue.config.productionTip = false 12 | 13 | new Vue({ 14 | router, 15 | store, 16 | render: h => h(Main), 17 | }).$mount('#app') 18 | -------------------------------------------------------------------------------- /src/route.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Register from './components/Auth/Register' 4 | import Login from './components/Auth/Login' 5 | import AllFood from './components/Food/AllFood' 6 | import SingleFood from './components/Food/SingleFood' 7 | import EditFood from './components/Food/EditFood' 8 | import CreateFood from './components/Food/CreateFood' 9 | 10 | Vue.use(VueRouter) 11 | 12 | const router = new VueRouter({ 13 | 14 | mode: 'history', 15 | routes: [ 16 | { 17 | path: '/', 18 | name: 'all_food', 19 | component: AllFood 20 | }, 21 | { 22 | path: '/single_food/:id', 23 | name: 'single_food', 24 | component: SingleFood 25 | }, 26 | { 27 | path: '/edit/single_food/:id', 28 | name: 'edit_food', 29 | component: EditFood, 30 | meta: { requiresAuth: true } 31 | }, 32 | { 33 | path: '/login', 34 | name: 'login', 35 | component: Login 36 | }, 37 | { 38 | path: '/register', 39 | name: 'register', 40 | component: Register 41 | }, 42 | { 43 | path: '/food', 44 | name: 'createFood', 45 | component: CreateFood, 46 | meta: { requiresAuth: true } 47 | }, 48 | ] 49 | }); 50 | 51 | router.beforeEach((to, from, next) => { 52 | if(to.path.startsWith("/login") || to.path.startsWith("/register")) { 53 | if (window.localStorage.getItem("access_token")) { 54 | next({ 55 | path: "/" 56 | }) 57 | } 58 | } 59 | if (to.meta.requiresAuth) { 60 | if (!window.localStorage.getItem("access_token")) { 61 | next({ 62 | path: "/login", 63 | query: { 64 | redirect: to.path 65 | } 66 | }); 67 | } else { 68 | next(); 69 | } 70 | } else { 71 | next(); 72 | } 73 | }) 74 | 75 | export default router; -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import axios from 'axios' 4 | import customAxios from '../authorization' 5 | 6 | import API_ROUTE from '../.env' 7 | 8 | Vue.use(Vuex) 9 | 10 | export const store = new Vuex.Store({ 11 | state: { 12 | all_food: [], 13 | food_and_creator: {}, 14 | user: localStorage.getItem('user') === null ? null : JSON.parse(localStorage.getItem('user')), 15 | appError: '', 16 | authenticated: localStorage.getItem('access_token') === null ? false : true, 17 | delMsg: '', 18 | }, 19 | 20 | getters: { 21 | allFood(state) { 22 | return state.all_food; 23 | }, 24 | food_and_creator(state){ 25 | return state.food_and_creator 26 | }, 27 | user(state){ 28 | return state.user 29 | } 30 | }, 31 | 32 | mutations: { 33 | appError(state, err) { 34 | state.appError = err 35 | }, 36 | getAllFood(state, all_food) { 37 | state.all_food = all_food 38 | state.appError = '' 39 | }, 40 | getFoodAndCreator(state, data) { 41 | state.food_and_creator = data 42 | state.appError = '' 43 | }, 44 | updatedFood(state, delMsg) { 45 | state.delMsg = delMsg 46 | state.appError = '' 47 | }, 48 | deletedFood(state, food) { 49 | state.food = food 50 | state.appError = '' 51 | }, 52 | loggedInUser(state, payload) { 53 | localStorage.setItem('access_token', payload.access_token) 54 | localStorage.setItem('refresh_token', payload.refresh_token) 55 | let user = { 56 | "id": payload.id, 57 | "first_name": payload.first_name, 58 | "last_name": payload.last_name, 59 | } 60 | localStorage.setItem('user', JSON.stringify(user)) 61 | state.user = user 62 | state.authenticated = true 63 | state.appError = '' 64 | }, 65 | createdFood(state, payload) { 66 | state.single_food = payload 67 | state.appError = '' 68 | }, 69 | logout(state){ 70 | localStorage.clear() 71 | state.user = null 72 | state.authenticated = false 73 | state.appError = '' 74 | } 75 | }, 76 | actions: { 77 | async register(context, payload) { 78 | try { 79 | await axios.post(`${API_ROUTE}/users`, { 80 | first_name: payload.first_name, 81 | last_name: payload.last_name, 82 | email: payload.email, 83 | password: payload.password 84 | }) 85 | }catch(err) { 86 | context.commit('appError', err.response ? err.response.data : null) 87 | } 88 | }, 89 | 90 | async getAllFood(context) { 91 | try { 92 | const res = await axios.get(`${API_ROUTE}/food`) 93 | context.commit('getAllFood', res.data) 94 | } catch(err) { 95 | console.log("this is the error getting the user: ", err) 96 | } 97 | }, 98 | async singleFood(context, payload) { 99 | try { 100 | const res = await axios.get(`${API_ROUTE}/food/${payload.food_id}`) 101 | context.commit('getFoodAndCreator', res.data) 102 | } catch(err) { 103 | console.log("this is the error getting the user: ", err) 104 | } 105 | }, 106 | async createFood(context, payload) { 107 | try { 108 | const res = await customAxios.post(`${API_ROUTE}/food`, payload) 109 | context.commit('createdFood', res.data) 110 | }catch(err) { 111 | context.commit('appError', err.response ? err.response.data : null) 112 | } 113 | }, 114 | async updateFood(context, payload) { 115 | try { 116 | const res = await customAxios.put(`${API_ROUTE}/food/${payload.food_id}`, payload.formData) 117 | context.commit('updatedFood', res.data) 118 | }catch(err) { 119 | context.commit('appError', err.response ? err.response.data : null) 120 | } 121 | }, 122 | async deleteFood(context, payload) { 123 | try { 124 | const res = await customAxios.delete(`${API_ROUTE}/food/${payload.food_id}`) 125 | context.commit('deletedFood', res.data) 126 | }catch(err) { 127 | context.commit('appError', err.response ? err.response.data : null) 128 | } 129 | }, 130 | async login(context, payload) { 131 | try { 132 | const res = await axios.post(`${API_ROUTE}/login`, { 133 | email: payload.email, 134 | password: payload.password 135 | }) 136 | context.commit('loggedInUser', res.data) 137 | }catch(err) { 138 | context.commit('appError', err.response ? err.response.data : null) 139 | } 140 | }, 141 | async logout(context) { 142 | try { 143 | const res = await customAxios.post(`${API_ROUTE}/logout`) 144 | context.commit('logout', res.data) 145 | }catch(err) { 146 | context.commit('appError', err.response ? err.response.data : null) 147 | } 148 | }, 149 | } 150 | }) 151 | --------------------------------------------------------------------------------