This commit is contained in:
Pünkösd Marcell 2020-11-26 01:30:15 +01:00
parent bd6f4f3827
commit 53e8aa9a9c
17 changed files with 566 additions and 111 deletions

View File

@ -0,0 +1 @@
VUE_APP_API_LOCATION=http://localhost:5000/

View File

@ -0,0 +1 @@
VUE_APP_API_LOCATION=https://spoton.k8s.kmlabz.com/api/

108
package-lock.json generated
View File

@ -1105,6 +1105,61 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true "dev": true
}, },
"@nuxt/opencollective": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.3.2.tgz",
"integrity": "sha512-XG7rUdXG9fcafu9KTDIYjJSkRO38EwjlKYIb5TQ/0WDbiTUTtUtgncMscKOYzfsY86kGs05pAuMOR+3Fi0aN3A==",
"requires": {
"chalk": "^4.1.0",
"consola": "^2.15.0",
"node-fetch": "^2.6.1"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"@soda/friendly-errors-webpack-plugin": { "@soda/friendly-errors-webpack-plugin": {
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.1.tgz", "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.1.tgz",
@ -2472,6 +2527,14 @@
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
"dev": true "dev": true
}, },
"axios": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
"integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"babel-eslint": { "babel-eslint": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
@ -2696,6 +2759,23 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true "dev": true
}, },
"bootstrap": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz",
"integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ=="
},
"bootstrap-vue": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.19.0.tgz",
"integrity": "sha512-IjAXUSrRU5Qu9x3uwUcoj6LtysKbCVeWoJOsODyI/WokStUr95M+tTIajXUjIrB/Nsk0fS+RNvZnm2sWeNFrhg==",
"requires": {
"@nuxt/opencollective": "^0.3.2",
"bootstrap": ">=4.5.3 <5.0.0",
"popper.js": "^1.16.1",
"portal-vue": "^2.1.7",
"vue-functional-data-merge": "^3.1.0"
}
},
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -3545,6 +3625,11 @@
"integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
"dev": true "dev": true
}, },
"consola": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
"integrity": "sha512-vlcSGgdYS26mPf7qNi+dCisbhiyDnrN1zaRbw3CSuc2wGOMEGGPsp46PdRG5gqXwgtJfjxDkxRNAgRPr1B77vQ=="
},
"console-browserify": { "console-browserify": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
@ -5435,8 +5520,7 @@
"follow-redirects": { "follow-redirects": {
"version": "1.13.0", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
"integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
"dev": true
}, },
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
@ -7440,6 +7524,11 @@
"lower-case": "^1.1.1" "lower-case": "^1.1.1"
} }
}, },
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"node-forge": { "node-forge": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
@ -8105,6 +8194,16 @@
"ts-pnp": "^1.1.6" "ts-pnp": "^1.1.6"
} }
}, },
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"portal-vue": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-2.1.7.tgz",
"integrity": "sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g=="
},
"portfinder": { "portfinder": {
"version": "1.0.28", "version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
@ -10861,6 +10960,11 @@
} }
} }
}, },
"vue-functional-data-merge": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
"integrity": "sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA=="
},
"vue-hot-reload-api": { "vue-hot-reload-api": {
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",

View File

@ -8,6 +8,9 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.0",
"bootstrap": "^4.5.3",
"bootstrap-vue": "^2.19.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-router": "^3.2.0", "vue-router": "^3.2.0",

View File

@ -1,32 +1,62 @@
<template> <template>
<div id="app"> <div id="app" v-if="$store.state.appReady">
<div id="nav">
<router-link to="/">Home</router-link> | <navbar/>
<router-link to="/about">About</router-link>
</div> <div id="content">
<router-view/> <router-view/>
</div> </div>
<div id="footer">
<footer-nav/>
</div>
</div>
<div id="app-loader" v-else>
<loading-screen/>
</div>
</template> </template>
<script>
import FooterNav from "@/components/FooterNav";
import Navbar from "@/components/Navbar";
import LoadingScreen from "@/components/LoadingScreen";
export default {
name: "Login",
components: {
Navbar,
FooterNav,
LoadingScreen
},
created() {
// The basic app is created... Currently showing a loading screen (as soon as mounted)
this.$store.dispatch('storeUserData','testuser').then(() => {
this.$store.dispatch('setAppReady');
});
}
}
</script>
<style> <style>
#content {
margin-top: 70px;
margin-bottom: 20px;
}
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; display: flex;
-webkit-font-smoothing: antialiased; flex-direction: column;
-moz-osx-font-smoothing: grayscale; min-height: 100vh;
text-align: center;
color: #2c3e50;
} }
#nav { #footer {
padding: 30px; padding: 1em;
margin-top: auto;
background: #E9ECEF;
} }
#nav a { #app-loader {
font-weight: bold; height: 100%;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
} }
</style> </style>

View File

@ -0,0 +1,134 @@
import axios from 'axios'
const API_BASE_URL = process.env.VUE_APP_API_LOCATION
const LOCAL_STORAGE_KEY = "JWT"
const COMMON_ERROR_CODES = {
403: "Access Denied",
401: "Authentication failed",
400: "Invalid Request",
417: "Invalid Request"
}
export default new class {
_setupHTTPObject() {
const token = localStorage.getItem(LOCAL_STORAGE_KEY)
let headers = {}
if (token) {
headers = {'Authorization': token}
}
this.http = axios.create({
baseURL: API_BASE_URL,
timeout: 15000, // 15 sec, mert szar a mobilnet
headers: headers
})
}
constructor() {
this._setupHTTPObject()
}
get haveToken() {
return !!localStorage.getItem(LOCAL_STORAGE_KEY)
}
_performApiCall(method, url, data, precheckToken, expectedStatus, errorTexts = COMMON_ERROR_CODES) {
return new Promise((resolve, reject) => {
if (precheckToken && !this.haveToken) {
return reject({
status: null,
text: "Not logged in",
data: null
});
}
this.http.request({
url, method, data,
validateStatus(status) {
return status === expectedStatus;
}
}).then((response) => {
return resolve(response.data);
}).catch((error) => {
if (!error.response) { // Network error (CORS?)
return reject({
status: null,
text: "Network error",
data: null
})
} else { // Server side error
return reject({
status: error.response.status,
text: errorTexts[error.response.status] || "Network or server error",
data: error.response.data
})
}
});
});
}
performLogin(name, password) {
return new Promise((resolve, reject) => {
this._performApiCall('post', '/auth/login', {name, password}, false, 201, {
401: "Invalid credentials",
...COMMON_ERROR_CODES
}).then((data) => {
localStorage.setItem(LOCAL_STORAGE_KEY, data.token)
this._setupHTTPObject() // Update JWT token memes
return resolve({name})
}).catch(reject)
});
}
performLogout() {
return new Promise((resolve, reject) => {
this._performApiCall('delete', '/auth/login', null, true, 204).then(() => {
localStorage.removeItem(LOCAL_STORAGE_KEY)
this._setupHTTPObject() // Update JWT token memes
return resolve(null)
}).catch(reject)
})
}
getMyInfo() {
return this._performApiCall('get', '/auth/me', null, true, 200);
}
getAllLists() {
return this._performApiCall('get', '/lists', null, true, 200);
}
getList(id) {
return this._performApiCall('get', `/lists/${id}`, null, true, 200);
}
getTrackFromList(listid, trackid) {
return this._performApiCall('get', `/lists/${listid}/${trackid}`, null, true, 200);
}
}

View File

@ -0,0 +1,20 @@
<template>
<b-container>
<b-row>
<b-col class="text-center">
<div>Listen to your favorite songs <b>on Spot</b>ify.</div>
<div class="text-muted">Copyright 2020 onSpot Team.</div>
</b-col>
</b-row>
</b-container>
</template>
<script>
export default {
name: "FooterNav"
}
</script>
<style scoped>
</style>

View File

@ -1,60 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -1,13 +1,16 @@
<template> <template>
$END$ <div id="loader-content" class="text-center">
<p>
<b-spinner variant="success" type="grow" />
</p>
<p>
onSpot is loading...
</p>
</div>
</template> </template>
<script>
export default {
name: "LoadingScreen"
}
</script>
<style scoped> <style scoped>
#loader-content {
margin: 45vh auto 0 auto;
}
</style> </style>

38
src/components/Navbar.vue Normal file
View File

@ -0,0 +1,38 @@
<template>
<b-navbar toggleable="sm" type="dark" variant="dark" fixed="top">
<b-navbar-brand>
<b>onSpot</b>
</b-navbar-brand>
<b-navbar-toggle target="nav-text-collapse"></b-navbar-toggle>
<b-collapse id="nav-text-collapse" is-nav>
<b-navbar-nav>
<b-nav-item v-if="$store.getters.isLoggedIn" to="/">My Collections</b-nav-item>
<b-nav-item v-else to="/login">Login</b-nav-item>
<b-nav-item to="/about">About</b-nav-item>
</b-navbar-nav>
<b-navbar-nav class="ml-auto" v-if="$store.getters.isLoggedIn">
<b-nav-item-dropdown :text="$store.state.userdata.name" right>
<b-dropdown-text>Lorem Ipsum dolor sit amaet</b-dropdown-text>
<b-dropdown-item href="#">Logout</b-dropdown-item>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</template>
<script>
export default {
name: "Navbar"
}
</script>
<style scoped>
</style>

View File

@ -1,9 +1,19 @@
import Vue from 'vue' import Vue from 'vue'
import App from './App.vue' import App from '@/App.vue'
import router from './router' import router from '@/router'
import store from './store' import store from '@/store'
import api from "@/api";
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.prototype.$api = api
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)
new Vue({ new Vue({
router, router,

View File

@ -1,6 +1,10 @@
import Vue from 'vue' import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import Home from '../views/Home.vue' import Home from '@/views/Home.vue'
import Login from "@/views/Login";
import store from '@/store'
Vue.use(VueRouter) Vue.use(VueRouter)
@ -8,7 +12,12 @@ const routes = [
{ {
path: '/', path: '/',
name: 'Home', name: 'Home',
component: Home component: Home,
meta: {
allowVisit(authorized) {
return authorized;
}
}
}, },
{ {
path: '/about', path: '/about',
@ -16,7 +25,22 @@ const routes = [
// route level code-splitting // route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route // this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
meta: {
allowVisit() {
return true;
}
}
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
allowVisit(authorized) {
return !authorized;
}
}
} }
] ]
@ -26,4 +50,24 @@ const router = new VueRouter({
routes routes
}) })
router.beforeEach((to, from, next) => {
const authorized = store.getters.isLoggedIn;
const visitAllowed = to.matched.some(record => record.meta.allowVisit(authorized))
if (visitAllowed) {
next();
return;
}
if (authorized) {
next({name: 'Home'})
} else {
next({name: 'Login'})
}
})
export default router export default router

View File

@ -5,11 +5,32 @@ Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
userdata: {
name: null
},
appReady: false
}, },
mutations: { mutations: {
storeUserData(state, username) {
state.userdata.name = username;
},
setAppReady(state) {
state.appReady = true;
}
}, },
actions: { actions: {
storeUserData({commit}, username) {
commit('storeUserData', username);
}, },
modules: { setAppReady({commit}) {
commit('setAppReady');
}
},
modules: {},
getters: {
isLoggedIn(state) {
return !!state.userdata.name;
}
} }
}) })

View File

@ -1,5 +1,7 @@
<template> <template>
<b-container>
<div class="about"> <div class="about">
<h1>This is an about page</h1> <h1>This is an about page</h1>
</div> </div>
</b-container>
</template> </template>

View File

@ -1,18 +1,17 @@
<template> <template>
<b-container>
<div class="home"> <div class="home">
<img alt="Vue logo" src="../assets/logo.png"> <h1>home</h1>
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div> </div>
</b-container>
</template> </template>
<script> <script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default { export default {
name: 'Home', name: 'Home',
components: { components: {
HelloWorld
} }
} }
</script> </script>

75
src/views/Login.vue Normal file
View File

@ -0,0 +1,75 @@
<template>
<b-container>
<b-row>
<b-col class="mx-auto" cols="12" lg="4" sm="8">
<b-card class="mt-5">
<div class="my-4 text-center">
<b-img src="@/assets/musicbrainz.svg" id="provider-banner"/>
</div>
<div class="mt-4">
<b-form @submit.prevent="performLogin" v-if="true">
<b-form-group
id="input-group-1"
label="Username:"
label-for="input-1"
>
<b-form-input
id="input-1"
v-model="form.username"
required
placeholder=""
autocomplete="off"
></b-form-input>
</b-form-group>
<b-form-group id="input-group-2" label="Password:" label-for="input-2"
description="Yes, the same one you use on MusicBrainz"
>
<b-form-input
id="input-2"
v-model="form.password"
required
placeholder=""
autocomplete="off"
type="password"
></b-form-input>
</b-form-group>
<div class="text-right">
<b-button type="submit" variant="primary">Login</b-button>
</div>
</b-form>
</div>
</b-card>
</b-col>
</b-row>
</b-container>
</template>
<script>
export default {
name: "Login",
data() {
return {
form: {
username: "",
password: ""
}
}
},
methods: {
performLogin() {
}
}
}
</script>
<style scoped>
#provider-banner {
width: 75%;
}
</style>

30
vue.config.js Normal file
View File

@ -0,0 +1,30 @@
module.exports = {
publicPath: "/",
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => {
options.transformAssetUrls = {
img: 'src',
image: 'xlink:href',
'b-avatar': 'src',
'b-img': 'src',
'b-img-lazy': ['src', 'blank-src'],
'b-card': 'img-src',
'b-card-img': 'src',
'b-card-img-lazy': ['src', 'blank-src'],
'b-carousel-slide': 'img-src',
'b-embed': 'src'
}
return options
}),
config.plugin('html')
.tap(args => {
args[0].title = "onSpot"
return args
})
}
}