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

View File

@ -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>

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>
$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
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 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,

View File

@ -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

View File

@ -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;
}
}
})

View File

@ -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>

View File

@ -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
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>