faszom
This commit is contained in:
68
src/App.vue
68
src/App.vue
@ -1,32 +1,62 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div id="nav">
|
||||
<router-link to="/">Home</router-link> |
|
||||
<router-link to="/about">About</router-link>
|
||||
<div id="app" v-if="$store.state.appReady">
|
||||
|
||||
<navbar/>
|
||||
|
||||
<div id="content">
|
||||
<router-view/>
|
||||
</div>
|
||||
<router-view/>
|
||||
|
||||
<div id="footer">
|
||||
<footer-nav/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="app-loader" v-else>
|
||||
<loading-screen/>
|
||||
</div>
|
||||
</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>
|
||||
#content {
|
||||
margin-top: 70px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#nav {
|
||||
padding: 30px;
|
||||
#footer {
|
||||
padding: 1em;
|
||||
margin-top: auto;
|
||||
background: #E9ECEF;
|
||||
}
|
||||
|
||||
#nav a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#nav a.router-link-exact-active {
|
||||
color: #42b983;
|
||||
#app-loader {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
134
src/api/index.js
134
src/api/index.js
@ -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);
|
||||
}
|
||||
|
||||
}
|
20
src/components/FooterNav.vue
Normal file
20
src/components/FooterNav.vue
Normal 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>
|
@ -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>
|
@ -1,13 +1,16 @@
|
||||
<template>
|
||||
$END$
|
||||
<div id="loader-content" class="text-center">
|
||||
<p>
|
||||
<b-spinner variant="success" type="grow" />
|
||||
</p>
|
||||
<p>
|
||||
onSpot is loading...
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "LoadingScreen"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
#loader-content {
|
||||
margin: 45vh auto 0 auto;
|
||||
}
|
||||
</style>
|
38
src/components/Navbar.vue
Normal file
38
src/components/Navbar.vue
Normal 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>
|
16
src/main.js
16
src/main.js
@ -1,9 +1,19 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import App from '@/App.vue'
|
||||
import router from '@/router'
|
||||
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.prototype.$api = api
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
Vue.use(IconsPlugin)
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
@ -1,6 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
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)
|
||||
|
||||
@ -8,7 +12,12 @@ const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
component: Home,
|
||||
meta: {
|
||||
allowVisit(authorized) {
|
||||
return authorized;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
@ -16,7 +25,22 @@ const routes = [
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// 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
|
||||
})
|
||||
|
||||
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
|
||||
|
@ -4,12 +4,33 @@ import Vuex from 'vuex'
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
},
|
||||
mutations: {
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
state: {
|
||||
userdata: {
|
||||
name: null
|
||||
},
|
||||
appReady: false
|
||||
},
|
||||
mutations: {
|
||||
storeUserData(state, username) {
|
||||
state.userdata.name = username;
|
||||
},
|
||||
setAppReady(state) {
|
||||
state.appReady = true;
|
||||
}
|
||||
|
||||
},
|
||||
actions: {
|
||||
storeUserData({commit}, username) {
|
||||
commit('storeUserData', username);
|
||||
},
|
||||
setAppReady({commit}) {
|
||||
commit('setAppReady');
|
||||
}
|
||||
},
|
||||
modules: {},
|
||||
getters: {
|
||||
isLoggedIn(state) {
|
||||
return !!state.userdata.name;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
<b-container>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</b-container>
|
||||
</template>
|
||||
|
@ -1,18 +1,17 @@
|
||||
<template>
|
||||
<b-container>
|
||||
<div class="home">
|
||||
<img alt="Vue logo" src="../assets/logo.png">
|
||||
<HelloWorld msg="Welcome to Your Vue.js App"/>
|
||||
<h1>home</h1>
|
||||
</div>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
HelloWorld
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
75
src/views/Login.vue
Normal file
75
src/views/Login.vue
Normal 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>
|
Reference in New Issue
Block a user