├── .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 |
2 |
3 |
45 |
46 |
47 |
48 |
49 |
50 |
84 |
85 |
--------------------------------------------------------------------------------
/src/components/Auth/Register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
57 |
58 |
59 |
60 |
61 |
62 |
98 |
99 |
--------------------------------------------------------------------------------
/src/components/Food/AllFood.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
![]()
13 |
14 |
15 |
16 | {{ titleShorten(food.title) }}
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
67 |
68 |
--------------------------------------------------------------------------------
/src/components/Food/CreateFood.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
48 |
49 |
50 |
51 |
52 |
113 |
114 |
--------------------------------------------------------------------------------
/src/components/Food/EditFood.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Edit Food
5 |
6 |
49 |
50 |
51 |
52 |
131 |
132 |
--------------------------------------------------------------------------------
/src/components/Food/SingleFood.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | created by {{ formatNames(creator.first_name, creator.last_name) }}
16 |
17 |
18 |
29 |
30 |
31 |
32 |
![]()
33 |
34 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
106 |
107 |
164 |
--------------------------------------------------------------------------------
/src/components/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/Nav/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
19 |
20 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/Nav/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | menu
7 |
8 |
9 | Food App
10 |
11 |
12 |
13 |
14 | Create
15 |
16 |
17 | Logout
18 |
19 |
20 |
21 |
22 | Login
23 |
24 |
25 | Register
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Food App
35 |
36 |
37 |
38 |
39 |
40 |
41 | Create Food
42 |
43 |
44 |
45 | Logout
46 |
47 |
48 |
49 |
50 |
51 | Login
52 |
53 |
54 |
55 |
56 | Register
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
70 |
71 |
72 |
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 |
--------------------------------------------------------------------------------