diff --git a/src/package.json b/src/package.json index 926ddfa0..4cb64c2f 100644 --- a/src/package.json +++ b/src/package.json @@ -1,34 +1,39 @@ { "private": true, "scripts": { "dev": "npm run development", "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", "watch": "npm run development -- --watch", "watch-poll": "npm run watch -- --watch-poll", "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", "prod": "npm run production", "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", "lint": "eslint --ext .js,.vue resources && stylelint \"resources/sass/*.scss\" \"resources/vue/components/*.vue\"" }, "devDependencies": { "@deveodk/vue-toastr": "^1.1.0", "axios": "^0.19", "bootstrap": "^4.4.1", "cross-env": "^5.1", "eslint": "^6.8.0", "eslint-plugin-vue": "^6.1.1", + "@fortawesome/fontawesome-svg-core": "^1.2.27", + "@fortawesome/free-brands-svg-icons": "^5.12.1", + "@fortawesome/free-regular-svg-icons": "^5.12.1", + "@fortawesome/free-solid-svg-icons": "^5.12.1", + "@fortawesome/vue-fontawesome": "^0.1.9", "jquery": "^3.4.1", "laravel-mix": "^4.0.7", "lodash": "^4.17.13", "popper.js": "^1.12", "resolve-url-loader": "^2.3.1", "sass": "^1.15.2", "sass-loader": "^7.1.0", "stylelint": "^12.0.1", "stylelint-config-standard": "^19.0.0", "vue": "^2.5.17", "vue-router": "^3.1.3", "vue-template-compiler": "^2.6.10", "vuex": "^3.1.1" } } diff --git a/src/resources/js/app.js b/src/resources/js/app.js index c4607432..2014bdba 100644 --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -1,179 +1,182 @@ /** * First we will load all of this project's JavaScript dependencies which * includes Vue and other libraries. It is a great starting point when * building robust, powerful web applications using Vue and Laravel. */ require('./bootstrap') window.Vue = require('vue') import AppComponent from '../vue/components/App' import MenuComponent from '../vue/components/Menu' import router from '../vue/js/routes.js' import store from '../vue/js/store' +import FontAwesomeIcon from './fontawesome.js' import VueToastr from '@deveodk/vue-toastr' +Vue.component('svg-icon', FontAwesomeIcon) + +Vue.use(VueToastr, { + defaultPosition: 'toast-bottom-right', + defaultTimeout: 5000 +}) + // Add a response interceptor for general/validation error handler // This have to be before Vue and Router setup. Otherwise we would // not be able to handle axios responses initiated from inside // components created/mounted handlers (e.g. signup code verification link) window.axios.interceptors.response.use( response => { // Do nothing return response }, error => { var error_msg if (error.response && error.response.status == 422) { error_msg = "Form validation error" $.each(error.response.data.errors || {}, (idx, msg) => { $('form').each((i, form) => { const input_name = ($(form).data('validation-prefix') || '') + idx const input = $('#' + input_name) if (input.length) { // Create an error message\ // API responses can use a string, array or object let msg_text = '' if ($.type(msg) !== 'string') { $.each(msg, (index, str) => { msg_text += str + ' ' }) } else { msg_text = msg } let feedback = $('
').text(msg_text) if (input.is('.listinput')) { // List input widget let list = input.next('.listinput-widget') list.children(':not(:first-child)').each((index, element) => { if (msg[index]) { $(element).find('input').addClass('is-invalid') } }) list.addClass('is-invalid').next('.invalid-feedback').remove() list.after(feedback) } else { // Standard form element input.addClass('is-invalid') input.parent().find('.invalid-feedback').remove() input.parent().append(feedback) } return false } }); }) $('form .is-invalid:not(.listinput-widget)').first().focus() } else if (error.response && error.response.data) { error_msg = error.response.data.message } else { error_msg = error.request ? error.request.statusText : error.message } app.$toastr('error', error_msg || "Server Error", 'Error') // Pass the error as-is return Promise.reject(error) } ) const app = new Vue({ el: '#app', components: { 'app-component': AppComponent, 'menu-component': MenuComponent }, store, router, data() { return { isLoading: true } }, methods: { // Clear (bootstrap) form validation state clearFormValidation(form) { $(form).find('.is-invalid').removeClass('is-invalid') $(form).find('.invalid-feedback').remove() }, // Set user state to "logged in" loginUser(token, dashboard) { store.commit('logoutUser') // destroy old state data store.commit('loginUser') localStorage.setItem('token', token) axios.defaults.headers.common.Authorization = 'Bearer ' + token if (dashboard !== false) { router.push(store.state.afterLogin || { name: 'dashboard' }) } store.state.afterLogin = null }, // Set user state to "not logged in" logoutUser() { store.commit('logoutUser') localStorage.setItem('token', '') delete axios.defaults.headers.common.Authorization router.push({ name: 'login' }) }, // Display "loading" overlay (to be used by route components) startLoading() { this.isLoading = true // Lock the UI with the 'loading...' element $('#app').append($('
Loading
')) }, // Hide "loading" overlay stopLoading() { $('#app > .app-loader').fadeOut() this.isLoading = false }, errorPage(code, msg) { // Until https://github.com/vuejs/vue-router/issues/977 is implemented // we can't really use router to display error page as it has two side // effects: it changes the URL and adds the error page to browser history. // For now we'll be replacing current view with error page "manually". const map = { 400: "Bad request", 401: "Unauthorized", 403: "Access denied", 404: "Not found", 405: "Method not allowed", 500: "Internal server error" } if (!msg) msg = map[code] || "Unknown Error" const error_page = `
${code}
${msg}
` $('#app').children(':not(nav)').remove() $('#app').append(error_page) }, errorHandler(error) { this.stopLoading() if (error.response.status === 401) { this.logoutUser() } else { this.errorPage(error.response.status, error.response.statusText) } } } }) - -Vue.use(VueToastr, { - defaultPosition: 'toast-bottom-right', - defaultTimeout: 5000 -}) diff --git a/src/resources/js/fontawesome.js b/src/resources/js/fontawesome.js new file mode 100644 index 00000000..5ed75156 --- /dev/null +++ b/src/resources/js/fontawesome.js @@ -0,0 +1,27 @@ + +import { library } from '@fortawesome/fontawesome-svg-core' +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' +//import { } from '@fortawesome/free-regular-svg-icons' +//import { } from '@fortawesome/free-brands-svg-icons' +import { + faCheck, + faGlobe, + faSyncAlt, + faUser, + faUserCog, + faUsers, + faWallet +} from '@fortawesome/free-solid-svg-icons' + +// Register only these icons we need +library.add( + faCheck, + faGlobe, + faSyncAlt, + faUser, + faUserCog, + faUsers, + faWallet +) + +export default FontAwesomeIcon diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss index f40ddca2..e6bf1195 100644 --- a/src/resources/sass/app.scss +++ b/src/resources/sass/app.scss @@ -1,104 +1,130 @@ // Fonts // Variables @import 'variables'; // Bootstrap @import '~bootstrap/scss/bootstrap'; // Toastr @import '~@deveodk/vue-toastr/dist/@deveodk/vue-toastr.css'; // Fixes Toastr incompatibility with Bootstrap .toast-container > .toast { opacity: 1; } @import 'menu'; nav + .container { margin-top: 120px; } #app { margin-bottom: 2rem; } #error-page { position: absolute; top: 0; height: 100%; width: 100%; align-items: center; display: flex; justify-content: center; color: #636b6f; z-index: 10; background: white; .code { text-align: right; border-right: 2px solid; font-size: 26px; padding: 0 15px; } .message { font-size: 18px; padding: 0 15px; } } .app-loader { background-color: $body-bg; height: 100%; width: 100%; position: absolute; top: 0; left: 0; display: flex; align-items: center; justify-content: center; .spinner-border { width: 120px; height: 120px; border-width: 15px; color: #b2aa99; } } pre { margin: 1rem 0; padding: 1rem; background-color: $menu-bg-color; } .card-title { font-size: 1.2rem; font-weight: bold; } .listinput { display: none; } .listinput-widget { & > div { &:not(:last-child) { margin-bottom: -1px; input, a.btn { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } } &:not(:first-child) { input, a.btn { border-top-right-radius: 0; border-top-left-radius: 0; } } } } + +#dashboard-nav { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-top: 1rem; + + & > a { + padding: 1rem; + text-align: center; + white-space: nowrap; + margin: 0 .5rem .5rem 0; + text-decoration: none; + min-width: 8rem; + + &.disabled { + pointer-events: none; + opacity: .6; + } + } + + svg { + width: 6rem; + height: 6rem; + } +} diff --git a/src/resources/vue/components/Dashboard.vue b/src/resources/vue/components/Dashboard.vue index 9d816701..44abd4f0 100644 --- a/src/resources/vue/components/Dashboard.vue +++ b/src/resources/vue/components/Dashboard.vue @@ -1,87 +1,96 @@ diff --git a/src/resources/vue/components/Domain/Info.vue b/src/resources/vue/components/Domain/Info.vue index 5c049eca..328bbf70 100644 --- a/src/resources/vue/components/Domain/Info.vue +++ b/src/resources/vue/components/Domain/Info.vue @@ -1,79 +1,79 @@ diff --git a/src/resources/vue/components/PasswordReset.vue b/src/resources/vue/components/PasswordReset.vue index a3a3a752..45bc62b9 100644 --- a/src/resources/vue/components/PasswordReset.vue +++ b/src/resources/vue/components/PasswordReset.vue @@ -1,155 +1,155 @@ diff --git a/src/resources/vue/components/Signup.vue b/src/resources/vue/components/Signup.vue index 91ec3325..3229d6a8 100644 --- a/src/resources/vue/components/Signup.vue +++ b/src/resources/vue/components/Signup.vue @@ -1,246 +1,246 @@ diff --git a/src/resources/vue/components/User/Info.vue b/src/resources/vue/components/User/Info.vue index c767d6af..e0d6236b 100644 --- a/src/resources/vue/components/User/Info.vue +++ b/src/resources/vue/components/User/Info.vue @@ -1,189 +1,189 @@ diff --git a/src/resources/vue/components/User/List.vue b/src/resources/vue/components/User/List.vue index 23916b9c..56068383 100644 --- a/src/resources/vue/components/User/List.vue +++ b/src/resources/vue/components/User/List.vue @@ -1,43 +1,45 @@ diff --git a/src/resources/vue/components/User/Profile.vue b/src/resources/vue/components/User/Profile.vue index a0087acc..fb900da7 100644 --- a/src/resources/vue/components/User/Profile.vue +++ b/src/resources/vue/components/User/Profile.vue @@ -1,101 +1,101 @@ diff --git a/src/tests/Browser/Pages/Dashboard.php b/src/tests/Browser/Pages/Dashboard.php index 851d98b2..37aad89c 100644 --- a/src/tests/Browser/Pages/Dashboard.php +++ b/src/tests/Browser/Pages/Dashboard.php @@ -1,45 +1,45 @@ assertPathIs('/dashboard') ->waitUntilMissing('@app .app-loader') - ->assertSee('Dashboard'); + ->assertVisible('@links'); } /** * Get the element shortcuts for the page. * * @return array */ public function elements() { return [ '@app' => '#app', '@links' => '#dashboard-nav', ]; } }