add project files
This commit is contained in:
parent
57a1013018
commit
a86bd04da2
6
.env.example
Normal file
6
.env.example
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
VUE_APP_SPOTIFY_CLIENT_ID=
|
||||||
|
VUE_APP_SPOTIFY_REDIRECT_URI="http://127.0.0.1:8080/auth/callback"
|
||||||
|
|
||||||
|
VUE_APP_API_BASEURL="/api/v1"
|
||||||
|
VUE_APP_API_AUTH_BASEURL="/api/auth"
|
||||||
|
VUE_APP_API_PUBLIC_BASEURL="/api/public"
|
15
.eslintrc.cjs
Executable file
15
.eslintrc.cjs
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-essential",
|
||||||
|
"eslint:recommended",
|
||||||
|
"@vue/eslint-config-typescript/recommended",
|
||||||
|
"@vue/eslint-config-prettier"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"vue/setup-compiler-macros": true
|
||||||
|
},
|
||||||
|
}
|
19
.eslintrc.js
19
.eslintrc.js
@ -1,19 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"plugin:vue/vue3-essential",
|
|
||||||
"eslint:recommended",
|
|
||||||
"@vue/typescript/recommended",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
|
||||||
},
|
|
||||||
};
|
|
1526
package-lock.json
generated
1526
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "spot2gether",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -8,7 +8,15 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@rushstack/eslint-patch": "^1.2.0",
|
||||||
|
"@vue/eslint-config-prettier": "^7.0.0",
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"bootstrap": "^5.2.1",
|
||||||
|
"bootstrap-darkmode": "^5.0.1",
|
||||||
|
"bootstrap-icons": "^1.9.1",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"localforage": "^1.10.0",
|
||||||
|
"querystring": "^0.2.1",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-router": "^4.0.3"
|
"vue-router": "^4.0.3"
|
||||||
|
81
src/Api.ts
Normal file
81
src/Api.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import type { AxiosInstance } from "axios";
|
||||||
|
import type { App } from "vue";
|
||||||
|
|
||||||
|
type ApiConfig = {
|
||||||
|
baseURL: string;
|
||||||
|
authBaseURL: string;
|
||||||
|
publicBaseURL: string;
|
||||||
|
accessToken: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Api {
|
||||||
|
private axios: AxiosInstance;
|
||||||
|
private axiosPublic: AxiosInstance;
|
||||||
|
private baseURL: string;
|
||||||
|
private authBaseURL: string;
|
||||||
|
private publicBaseURL: string;
|
||||||
|
private accessToken: string | null;
|
||||||
|
|
||||||
|
constructor({ baseURL, accessToken, authBaseURL, publicBaseURL }: ApiConfig) {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
this.authBaseURL = authBaseURL;
|
||||||
|
this.publicBaseURL = publicBaseURL;
|
||||||
|
this.accessToken = accessToken ?? localStorage.getItem("access-token");
|
||||||
|
this.axios = axios.create({ baseURL });
|
||||||
|
this.axios.interceptors.request.use(({ headers = {}, ...config }) => ({
|
||||||
|
...config,
|
||||||
|
headers: { ...headers, ["access-token"]: this.accessToken },
|
||||||
|
}));
|
||||||
|
this.axiosPublic = axios.create({ baseURL: publicBaseURL });
|
||||||
|
this.testConnection().catch(() => {
|
||||||
|
this.accessToken = null;
|
||||||
|
localStorage.removeItem("access-token");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async auth({ code, state }: { code: string; state: string }) {
|
||||||
|
const tokens = (await axios.post(`${this.authBaseURL}`, { code, state }))
|
||||||
|
?.data;
|
||||||
|
if (tokens.accessToken) {
|
||||||
|
this.accessToken = tokens.accessToken as string;
|
||||||
|
localStorage.setItem("access-token", this.accessToken);
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
throw "got no access token from spotify :/";
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthorized() {
|
||||||
|
return !!this.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection() {
|
||||||
|
return (await this.axios.get(`/test`))?.data;
|
||||||
|
}
|
||||||
|
async getRole() {
|
||||||
|
return (await this.axios.get(`/user/role`))?.data;
|
||||||
|
}
|
||||||
|
async getCurrentlyPlaying() {
|
||||||
|
return (await this.axios.get(`/user/currentlyPlaying`))?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserInfo(userId: string) {
|
||||||
|
return (await this.axiosPublic.get(`/users/${userId}/info`))?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async joinSession(userId: string) {
|
||||||
|
return (await this.axios.post(`/user/joinSession`, { userId }))?.data;
|
||||||
|
}
|
||||||
|
async leaveSession(userId: string) {
|
||||||
|
return (await this.axios.post(`/user/leaveSession`, { userId }))?.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let api = null as Api | null;
|
||||||
|
|
||||||
|
export const useApi = () => api;
|
||||||
|
|
||||||
|
export const createApi = (vue: App, config: ApiConfig) => {
|
||||||
|
api = new Api(config);
|
||||||
|
vue.config.globalProperties.$api = api;
|
||||||
|
};
|
40
src/App.vue
40
src/App.vue
@ -1,29 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav>
|
<div>
|
||||||
<router-link to="/">Home</router-link> |
|
<div class="bg-secondary shadow">
|
||||||
<router-link to="/about">About</router-link>
|
<nav class="navbar px-2 container">
|
||||||
|
<router-link class="d-flex btn" to="/">
|
||||||
|
<div class="d-none d-sm-flex header-title flex-column justify-content-center">
|
||||||
|
<b>spot2gether</b>
|
||||||
|
<div>music connects</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
<div>
|
||||||
|
<router-link
|
||||||
|
v-if="!$api.isAuthorized()"
|
||||||
|
to="/auth"
|
||||||
|
class="btn btn-outline-dark"
|
||||||
|
>
|
||||||
|
login
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="container my-5">
|
||||||
|
<main>
|
||||||
<router-view />
|
<router-view />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#app {
|
@import "main.scss";
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
padding: 30px;
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #2c3e50;
|
|
||||||
|
|
||||||
&.router-link-exact-active {
|
&.router-link-exact-active {
|
||||||
color: #42b983;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
src/assets/throbber.svg
Normal file
9
src/assets/throbber.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg width='200px' height='200px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring-alt">
|
||||||
|
<rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect>
|
||||||
|
<circle cx="50" cy="50" r="40" stroke="#d9d9d9" fill="none" stroke-width="10" stroke-linecap="round"></circle>
|
||||||
|
<circle cx="50" cy="50" r="40" stroke="#009eff" fill="none" stroke-width="10" stroke-linecap="round">
|
||||||
|
<animate attributeName="stroke-dashoffset" dur="2s" repeatCount="indefinite" from="502" to="0"></animate>
|
||||||
|
<animate attributeName="stroke-dasharray" dur="2s" repeatCount="indefinite" values="125.5 125.5;1 250;125.5 125.5"></animate>
|
||||||
|
</circle>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 751 B |
54
src/components/CurrentlyPlaying.vue
Normal file
54
src/components/CurrentlyPlaying.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
currentlyPlaying: Object,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="currentlyPlaying card">
|
||||||
|
<div class="card-header">
|
||||||
|
<b>{{ currentlyPlaying.item.name }}</b>
|
||||||
|
{{ currentlyPlaying.item.artists.map(artist => artist.name).join(', ') }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<img :src="currentlyPlaying.item.album.images[0].url" alt="album cover" class="card-img">
|
||||||
|
<p>
|
||||||
|
listening from {{ currentlyPlaying.context.type }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md">
|
||||||
|
<p class="my-2">
|
||||||
|
<a
|
||||||
|
:href="currentlyPlaying.item.externalURL.spotify"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener norefferrer"
|
||||||
|
class="btn btn-outline-dark m-1"
|
||||||
|
>
|
||||||
|
view track on Spotify
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="currentlyPlaying.context.externalURL.spotify"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener norefferrer"
|
||||||
|
class="btn btn-outline-dark m-1"
|
||||||
|
>
|
||||||
|
view playlist on Spotify
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.currentlyPlaying {
|
||||||
|
.card-img {
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,140 +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-pwa"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>pwa</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-eslint"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>eslint</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>typescript</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 lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "HelloWorld",
|
|
||||||
props: {
|
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped lang="scss">
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
90
src/components/PromiseResolver.vue
Normal file
90
src/components/PromiseResolver.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="position-relative PromiseResolver"
|
||||||
|
:class="{ loading: loading && showThrobber }"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
v-if="!error && (!loading || (overlay && data !== null))"
|
||||||
|
:data="getter(data)"
|
||||||
|
:update="update"
|
||||||
|
/>
|
||||||
|
<slot v-if="error" name="error">
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{ error.message || 'Oops! Something went wrong :/' }}
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
<slot v-else-if="loading && showThrobber" name="loading">
|
||||||
|
<div class="overlay">
|
||||||
|
<div class="wrapper">
|
||||||
|
<ThrobberLoading />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps } from "vue";
|
||||||
|
import ThrobberLoading from "@/components/ThrobberLoading.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
promise: [Promise, Object],
|
||||||
|
getter: {
|
||||||
|
type: Function,
|
||||||
|
default: (data: unknown) => data,
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const showThrobber = ref(true);
|
||||||
|
const error = ref(null);
|
||||||
|
const data = ref(null);
|
||||||
|
|
||||||
|
const update = async (promise: Promise | unknown) => {
|
||||||
|
loading.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (loading.value) showThrobber.value = true;
|
||||||
|
}, 100);
|
||||||
|
try {
|
||||||
|
data.value = await (promise.isPromiseList
|
||||||
|
? Promise.all(promise.promises)
|
||||||
|
: promise);
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
update(props.promise);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.PromiseResolver {
|
||||||
|
&.loading {
|
||||||
|
min-height: 5rem;
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 5rem;
|
||||||
|
background-color: #fffd;
|
||||||
|
z-index: 9000;
|
||||||
|
.wrapper {
|
||||||
|
position: sticky;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
19
src/components/ThrobberLoading.vue
Normal file
19
src/components/ThrobberLoading.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div class="Throbber">
|
||||||
|
<img src="../assets/throbber.svg" alt="throbber" />
|
||||||
|
<div class="info"><slot>loading</slot></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.Throbber {
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
align-items: center;
|
||||||
|
img {
|
||||||
|
height: 3rem;
|
||||||
|
width: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
6
src/composables/useAdminApi.ts
Normal file
6
src/composables/useAdminApi.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { getCurrentInstance } from "vue";
|
||||||
|
|
||||||
|
export const useAdminApi = () => {
|
||||||
|
const vm = getCurrentInstance();
|
||||||
|
return vm?.appContext.config.globalProperties.$adminApi;
|
||||||
|
};
|
14
src/main.scss
Normal file
14
src/main.scss
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@import "bootstrap/scss/bootstrap.scss";
|
||||||
|
//@import "bootstrap-darkmode/scss/darktheme.scss";
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
//background-color: $dark-body-bg;
|
||||||
|
line-break: loose;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
10
src/main.ts
10
src/main.ts
@ -2,5 +2,13 @@ import { createApp } from "vue";
|
|||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import "./registerServiceWorker";
|
import "./registerServiceWorker";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
|
import { createApi } from "@/Api";
|
||||||
|
|
||||||
createApp(App).use(router).mount("#app");
|
createApp(App)
|
||||||
|
.use(router)
|
||||||
|
.use(createApi, {
|
||||||
|
baseURL: process.env.VUE_APP_API_BASEURL,
|
||||||
|
authBaseURL: process.env.VUE_APP_API_AUTH_BASEURL,
|
||||||
|
publicBaseURL: process.env.VUE_APP_API_PUBLIC_BASEURL,
|
||||||
|
})
|
||||||
|
.mount("#app");
|
||||||
|
18
src/middlewares/auth.ts
Normal file
18
src/middlewares/auth.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useApi } from "@/Api";
|
||||||
|
import type {
|
||||||
|
NavigationGuardNext,
|
||||||
|
RouteLocationNormalized,
|
||||||
|
Router,
|
||||||
|
} from "vue-router";
|
||||||
|
|
||||||
|
type Context = {
|
||||||
|
from: RouteLocationNormalized;
|
||||||
|
to: RouteLocationNormalized;
|
||||||
|
next: NavigationGuardNext;
|
||||||
|
router: Router;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const auth = ({ next }: Context) => {
|
||||||
|
if (!useApi()?.isAuthorized()) next({ name: "auth" });
|
||||||
|
else next();
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||||
import HomeView from "../views/HomeView.vue";
|
import { auth } from "@/middlewares/auth";
|
||||||
|
import HomeView from "../views/HomePage.vue";
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
@ -8,13 +9,31 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: HomeView,
|
component: HomeView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/about",
|
path: "/connect",
|
||||||
name: "about",
|
name: "connect",
|
||||||
// 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: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
|
import(/* webpackChunkName: "connect" */ "../views/ConnectPage.vue"),
|
||||||
|
meta: {
|
||||||
|
requireAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/user/:id",
|
||||||
|
name: "user",
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "user" */ "../views/UserPage.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/auth",
|
||||||
|
name: "auth",
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "auth" */ "../views/AuthPage.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/auth/callback",
|
||||||
|
name: "authCallback",
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "auth" */ "../views/AuthCallbackPage.vue"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -23,4 +42,10 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
if (!to.name) next("/");
|
||||||
|
else if (to.meta?.requireAuth) auth({ to, from, next, router });
|
||||||
|
else next();
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
21
src/views/AuthCallbackPage.vue
Normal file
21
src/views/AuthCallbackPage.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
import PromiseResolver from "@/components/PromiseResolver.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Connect to Spotify</h1>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<PromiseResolver :promise="$api.auth({ code, state })">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
Authorization completed
|
||||||
|
</div>
|
||||||
|
<router-link to="/connect" class="btn btn-primary">
|
||||||
|
Join a Session
|
||||||
|
</router-link>
|
||||||
|
</PromiseResolver>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
35
src/views/AuthPage.vue
Normal file
35
src/views/AuthPage.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script setup>
|
||||||
|
import querystring from "querystring";
|
||||||
|
|
||||||
|
const randomString = (length) => {
|
||||||
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let result = '';
|
||||||
|
for ( let i = 0; i < length; i++ )
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authUrl = 'https://accounts.spotify.com/authorize?' + querystring.stringify({
|
||||||
|
response_type: 'code',
|
||||||
|
// eslint-disable-next-line
|
||||||
|
client_id: process.env.VUE_APP_SPOTIFY_CLIENT_ID,
|
||||||
|
// eslint-disable-next-line
|
||||||
|
redirect_uri: process.env.VUE_APP_SPOTIFY_REDIRECT_URI,
|
||||||
|
scope: 'user-read-email app-remote-control user-read-playback-state user-read-currently-playing user-modify-playback-state',
|
||||||
|
state: randomString(16),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Login</h1>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
Authenticate with Spotify
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<a class="btn btn-primary" :href="authUrl">Authenticate</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
54
src/views/ConnectPage.vue
Normal file
54
src/views/ConnectPage.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import PromiseResolver from "@/components/PromiseResolver.vue";
|
||||||
|
import CurrentlyPlaying from "@/components/CurrentlyPlaying.vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="about">
|
||||||
|
<h1>Connect</h1>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
test
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<PromiseResolver
|
||||||
|
:promise="$api.testConnection()"
|
||||||
|
v-slot="{ data }"
|
||||||
|
>
|
||||||
|
{{ data }}
|
||||||
|
</PromiseResolver>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
role
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<PromiseResolver
|
||||||
|
:promise="$api.getRole()"
|
||||||
|
v-slot="{ data }"
|
||||||
|
>
|
||||||
|
{{ data }}
|
||||||
|
</PromiseResolver>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
currently playing
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<PromiseResolver
|
||||||
|
:promise="$api.getCurrentlyPlaying()"
|
||||||
|
v-slot="{ data: { currentlyPlaying }, update }"
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<CurrentlyPlaying :currently-playing="currentlyPlaying" />
|
||||||
|
<button @click="update($api.getCurrentlyPlaying())" class="btn btn-secondary">update</button>
|
||||||
|
</PromiseResolver>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
8
src/views/HomePage.vue
Normal file
8
src/views/HomePage.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<h1>Spot2Gether</h1>
|
||||||
|
<p>
|
||||||
|
listen to music with your friends
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -1,18 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="home">
|
|
||||||
<img alt="Vue logo" src="../assets/logo.png" />
|
|
||||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "HomeView",
|
|
||||||
components: {
|
|
||||||
HelloWorld,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
45
src/views/UserPage.vue
Normal file
45
src/views/UserPage.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import PromiseResolver from "@/components/PromiseResolver.vue";
|
||||||
|
import CurrentlyPlaying from "@/components/CurrentlyPlaying.vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>User</h1>
|
||||||
|
<PromiseResolver
|
||||||
|
:promise="$api.getUserInfo($route.params.id)"
|
||||||
|
v-slot="{ data: { user, currentlyPlaying } }"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
{{ user.displayName }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<img :src="user.images[0].url" alt="user image" class="card-img">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="$api.isAuthorized()"
|
||||||
|
@click="$api.joinSession($route.params.id)"
|
||||||
|
class="btn btn-primary my-2"
|
||||||
|
>
|
||||||
|
Join Session
|
||||||
|
</button>
|
||||||
|
<router-link
|
||||||
|
v-else
|
||||||
|
to="/auth"
|
||||||
|
class="btn btn-primary my-2"
|
||||||
|
>
|
||||||
|
login with Spotify and join session
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<h2>Currently listening to:</h2>
|
||||||
|
<CurrentlyPlaying v-if="currentlyPlaying?.item" :currently-playing="currentlyPlaying" />
|
||||||
|
</div>
|
||||||
|
</PromiseResolver>
|
||||||
|
</template>
|
Loading…
Reference in New Issue
Block a user