Make app function.
This commit is contained in:
parent
e4c81beefe
commit
af75bbc522
14 changed files with 865 additions and 0 deletions
71
app.js
Normal file
71
app.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
let app = new Vue({
|
||||
el: `#app`,
|
||||
data: {
|
||||
api_base: `https://api.spotify.com/v1`,
|
||||
duration: ``,
|
||||
type: ``,
|
||||
count: ``,
|
||||
show: {
|
||||
popularity_popup: false,
|
||||
popularity_hover: false,
|
||||
follower_hover: false
|
||||
},
|
||||
error: {
|
||||
main: ``,
|
||||
auth: ``,
|
||||
},
|
||||
auth: {
|
||||
alert: `We will only be able to access your top tracks and artists, nothing else. This is also only done on your browser. Our servers do not see any of the data from your account.`,
|
||||
base_url: `https://accounts.spotify.com/authorize`,
|
||||
redirect: `http://localhost:5000`,
|
||||
client_id: `3a1795e9d55445b0aa0c05dd74c866fb`,
|
||||
scopes: [
|
||||
`user-top-read`
|
||||
],
|
||||
show_dialog: true,
|
||||
use_state: false
|
||||
},
|
||||
user: {
|
||||
name: ``,
|
||||
image: ``
|
||||
},
|
||||
data: {
|
||||
tracks: [],
|
||||
artists: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
spotify_auth_url: auth_url,
|
||||
is_authed: verify_auth,
|
||||
button_type: get_button_text,
|
||||
},
|
||||
methods: {
|
||||
get_token: function () {
|
||||
let params = new URLSearchParams(window.location.hash.slice(1));
|
||||
return params.get(`access_token`);
|
||||
},
|
||||
get_user: function () {
|
||||
axios.get(
|
||||
`${this.api_base}/me`,
|
||||
{ headers: { Authorization: `Bearer ${this.get_token()}` } }
|
||||
).then((response) => {
|
||||
if (response.error) {
|
||||
window.location.hash = ``;
|
||||
window.location.href = this.auth.redirect;
|
||||
return
|
||||
}
|
||||
let data = response.data;
|
||||
|
||||
// Set the Vue user object
|
||||
this.user.name = data.display_name;
|
||||
this.user.image = data.images.length > 0 ? data.images[0].url : ``;
|
||||
|
||||
}).catch((err) => {
|
||||
window.location.hash = ``;
|
||||
window.location.href = this.auth.redirect;
|
||||
return
|
||||
})
|
||||
},
|
||||
get_data: fetch_data,
|
||||
}
|
||||
})
|
||||
14
components/artist.html
Normal file
14
components/artist.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<div class="artist">
|
||||
<div class="image profile_pic">
|
||||
<img :src="artist.image.url" :alt="artist.name + `'s profile picture`">
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="name">
|
||||
<a :href="artist.link" target="_blank" rel="noopener">{{artist.name}}</a>
|
||||
</span>
|
||||
<br>
|
||||
<span class="genres">{{artist.genres.join(", ")}}</span>
|
||||
</div>
|
||||
<div class="popularity">{{artist.popularity}}</div>
|
||||
<div class="followers">{{artist.follower_count}}</div>
|
||||
</div>
|
||||
24
components/artist.js
Normal file
24
components/artist.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
Vue.component(
|
||||
`artist`,
|
||||
{
|
||||
props: [ `artist` ],
|
||||
data: function () {
|
||||
return {};
|
||||
},
|
||||
computed: {},
|
||||
template: `<div class="artist">
|
||||
<div class="image profile_pic">
|
||||
<img :src="artist.image.url" :alt="artist.name + \`'s profile picture\`">
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="name">
|
||||
<a :href="artist.link" target="_blank" rel="noopener">{{artist.name}}</a>
|
||||
</span>
|
||||
<br>
|
||||
<span class="genres">{{artist.genres.join(", ")}}</span>
|
||||
</div>
|
||||
<div class="popularity">{{artist.popularity}}</div>
|
||||
<div class="followers">{{artist.follower_count}}</div>
|
||||
</div>`
|
||||
}
|
||||
)
|
||||
0
components/icons.js
Normal file
0
components/icons.js
Normal file
20
components/track.html
Normal file
20
components/track.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<div class="track">
|
||||
<div class="cover center">
|
||||
<a :href="track.album.link" target="_blank" rel="noopener">
|
||||
<img :src="track.album.image.url" :alt="track.album.name + ' Cover Image'" class="cover">
|
||||
</a>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="name remote" v-if="!track.locality">
|
||||
<a :href="track.link" target="_blank" rel="noopener">{{track.name}}</a>
|
||||
</span>
|
||||
<span class="name local" v-else>
|
||||
{{track.name}}
|
||||
</span>
|
||||
<br>
|
||||
<span class="artist" v-html="artists"></span>
|
||||
<br>
|
||||
</div>
|
||||
<div class="popularity" v-tooltip="popularity_tooltip">{{track.popularity}}</div>
|
||||
<div class="duration">{{duration}}</div>
|
||||
</div>
|
||||
63
components/track.js
Normal file
63
components/track.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
Vue.component(
|
||||
`track-card`,
|
||||
{
|
||||
props: [ `track` ],
|
||||
data: function () {
|
||||
return {
|
||||
popularity_tooltip: `Popularity.\nClick for more information.`
|
||||
}
|
||||
},
|
||||
template: `<div class="track">
|
||||
<div class="cover center">
|
||||
<a :href="track.album.link" target="_blank" rel="noopener">
|
||||
<img :src="track.album.image.url" :alt="track.album.name + ' Cover Image'" class="cover">
|
||||
</a>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="name remote" v-if="!track.locality">
|
||||
<a :href="track.link" target="_blank" rel="noopener">{{track.name}}</a>
|
||||
</span>
|
||||
<span class="name local" v-else>
|
||||
{{track.name}}
|
||||
</span>
|
||||
<br>
|
||||
<span class="artist" v-html="artists"></span>
|
||||
<br>
|
||||
</div>
|
||||
<div class="popularity" v-tooltip="popularity_tooltip">{{track.popularity}}</div>
|
||||
<div class="duration">{{duration}}</div>
|
||||
</div>`,
|
||||
computed: {
|
||||
duration: function () {
|
||||
let timestamp = ``;
|
||||
|
||||
// Converting to seconds
|
||||
let duration = Math.trunc(this.track.duration / 1000);
|
||||
let seconds = duration % 60;
|
||||
|
||||
// Converting to minutes
|
||||
duration = Math.trunc(duration / 60);
|
||||
let minutes = duration % 60
|
||||
|
||||
// Converting to hours
|
||||
duration = Math.trunc(duration / 60);
|
||||
let hours = duration % 24;
|
||||
|
||||
if (seconds < 10) {
|
||||
seconds = `0${seconds}`
|
||||
};
|
||||
|
||||
return `${hours > 0 ? `${hours}:` : ''}${minutes}:${seconds}`;
|
||||
},
|
||||
artists: function () {
|
||||
let artists = [];
|
||||
for (var artist of this.track.artists) {
|
||||
artists.push(
|
||||
`<a href="${artist.external_urls.spotify}" target="_blank" rel="noopener">${artist.name}</a>`
|
||||
)
|
||||
}
|
||||
return artists.join(`, `)
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
84
css/artist.css
Normal file
84
css/artist.css
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
div.artist {
|
||||
background-color: var(--card-colour);
|
||||
color: var(--card-text);
|
||||
border-radius: 7px;
|
||||
border-style: none;
|
||||
padding: 10px;
|
||||
padding-top: 20px;
|
||||
margin: 5px auto;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
div.artist > div.profile_pic {
|
||||
text-align: center;
|
||||
}
|
||||
div.artist > div.profile_pic img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
div.artist > div.info {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
div.artist > div.info > span.name {
|
||||
text-decoration: none;
|
||||
color: var(--text-on-card);
|
||||
vertical-align: middle;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
div.artist > .followers {
|
||||
background-color: var(--on-card-colour);
|
||||
color: var(--on-card-text);
|
||||
vertical-align: middle;
|
||||
position: absolute;
|
||||
padding: 1px 6px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
|
||||
/* top-left top-right lower-right lower-left */
|
||||
border-radius: 7px 0 7px 0;
|
||||
}
|
||||
|
||||
div.artist > div.info > span.genres {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
|
||||
div.artist > .popularity {
|
||||
background-color: var(--on-card-colour);
|
||||
color: var(--on-card-text);
|
||||
position: absolute;
|
||||
padding: 1px 6px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
|
||||
/* top-left top-right lower-right lower-left */
|
||||
border-radius: 0px 7px 0px 7px;
|
||||
}
|
||||
|
||||
div.artist a {
|
||||
color: var(--text-on-card);
|
||||
text-decoration: none;
|
||||
}
|
||||
div.artist a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/* DESKTOP STYLES */
|
||||
@media only screen and (min-width: 768px) {
|
||||
div.artist {
|
||||
width: 230px;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
84
css/track.css
Normal file
84
css/track.css
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
div.track {
|
||||
background-color: var(--card-colour);
|
||||
color: var(--card-text);
|
||||
border-radius: 7px;
|
||||
border-style: none;
|
||||
padding: 10px;
|
||||
padding-top: 20px;
|
||||
margin: 5px auto;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
div.track > div.cover {
|
||||
text-align: center;
|
||||
}
|
||||
div.track > div.cover img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
div.track > div.info {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
div.track > div.info > span.name {
|
||||
text-decoration: none;
|
||||
color: var(--text-on-card);
|
||||
vertical-align: middle;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
div.track > .duration {
|
||||
background-color: var(--on-card-colour);
|
||||
color: var(--on-card-text);
|
||||
vertical-align: middle;
|
||||
position: absolute;
|
||||
padding: 1px 6px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
|
||||
/* top-left top-right lower-right lower-left */
|
||||
border-radius: 7px 0 7px 0;
|
||||
}
|
||||
|
||||
div.track > div.info > span.artist {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
|
||||
div.track > .popularity {
|
||||
background-color: var(--on-card-colour);
|
||||
color: var(--on-card-text);
|
||||
position: absolute;
|
||||
padding: 1px 6px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
|
||||
/* top-left top-right lower-right lower-left */
|
||||
border-radius: 0px 7px 0px 7px;
|
||||
}
|
||||
|
||||
div.track a {
|
||||
color: var(--text-on-card);
|
||||
text-decoration: none;
|
||||
}
|
||||
div.track a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
/* DESKTOP STYLES */
|
||||
@media only screen and (min-width: 768px) {
|
||||
div.track {
|
||||
width: 230px;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
1
expired_tokens
Normal file
1
expired_tokens
Normal file
|
|
@ -0,0 +1 @@
|
|||
BQCai7XSoBzX6DhHscdWeWOeg_0J6KC6-mL2YFDzvQsWmlGUFMdXCTFji0TpxpSX-bak1GretW99OyyvXAY4xiECbou6H2_wDq8Zq-Yzr9UUeph0zF57BZ3nZjLA0vMDu3AXzGMQqPml9POL
|
||||
82
index.html
Normal file
82
index.html
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Top Lists For Spotify</title>
|
||||
|
||||
<!-- Stylesheets -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="./css/track.css">
|
||||
<link rel="stylesheet" href="./css/artist.css">
|
||||
|
||||
<!-- Javascript Imports -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
||||
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||
<script src="./components/artist.js" defer async></script>
|
||||
<script src="./components/track.js" defer async></script>
|
||||
<script src="https://unpkg.com/v-tooltip"></script>
|
||||
<script src="./js/text_computation.js"></script>
|
||||
<script src="./app.js" defer></script>
|
||||
<script src="./js/auth.js"></script>
|
||||
<script src="./js/data.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" v-cloak>
|
||||
<div id="login" v-if="!is_authed">
|
||||
<div class="card center">
|
||||
<a :href="spotify_auth_url">
|
||||
<button id="spotify-login">Login With Spotify</button>
|
||||
</a>
|
||||
<p class="error" v-if="error.auth">{{error.auth}}</p>
|
||||
<p>{{auth.alert}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main" v-else v-cloak>
|
||||
<div class="flex-row">
|
||||
<div v-cloak class="account-info">
|
||||
<img v-if="user.image" :src="user.image" :alt="`${user.name}'s profile picture`" class="profile-picture">
|
||||
{{user.name}}
|
||||
</div>
|
||||
<div class="type">
|
||||
<select v-model="type">
|
||||
<option value="" disabled>Please Select A Type</option>
|
||||
<option value="Tracks">Tracks</option>
|
||||
<option value="Artists">Artists</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="duration">
|
||||
<select v-model="duration">
|
||||
<option value="" disabled>Please Select A Duration</option>
|
||||
<option value="long_term">Several Years</option>
|
||||
<option value="medium_term">~6 Months</option>
|
||||
<option value="short_term">~4 Weeks</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="limit">
|
||||
<input type="number" v-model="count" placeholder="How Many?">
|
||||
</div>
|
||||
<div id="submit-button" v-if="button_type && duration">
|
||||
<button @click="get_data()">Get Top {{count > 1 ? count : ``}} {{button_type}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row error" v-if="error.main">{{error.main}}</div>
|
||||
<div class="body" v-if="data.tracks.length > 0">
|
||||
<track-card
|
||||
v-for="top_track in data.tracks"
|
||||
:track="top_track"
|
||||
:key="top_track.id"
|
||||
></track-card>
|
||||
</div>
|
||||
<div class="body" v-if="data.artists.length > 0">
|
||||
<artist
|
||||
v-for="top_artist in data.artists"
|
||||
:artist="top_artist"
|
||||
:key="top_artist.id"
|
||||
></artist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
53
js/auth.js
Normal file
53
js/auth.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
function auth_url () {
|
||||
let params = [
|
||||
`client_id=${this.auth.client_id}`,
|
||||
`response_type=token`,
|
||||
`redirect_uri=${encodeURIComponent(this.auth.redirect)}`,
|
||||
`scope=${encodeURIComponent(this.auth.scopes.join(" "))}`,
|
||||
`show_dialog=${this.auth.show_dialog}`
|
||||
];
|
||||
|
||||
// Create the state data if we are using it
|
||||
if (this.auth.use_state) {
|
||||
let state = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
params.push(`state=${state}`);
|
||||
localStorage.setItem(`top-spotify-state`, state);
|
||||
};
|
||||
|
||||
return `${this.auth.base_url}?${params.join("&")}`;
|
||||
};
|
||||
|
||||
|
||||
function verify_auth () {
|
||||
let params = new URLSearchParams(window.location.hash.slice(1));
|
||||
|
||||
// Check to ensure the authorization was a success
|
||||
if (params.get(`access_token`)) {
|
||||
this.get_user()
|
||||
|
||||
// Check if we compare state
|
||||
if (this.use_state) {
|
||||
|
||||
// Compare given state to localstorage state
|
||||
let LS_state = localStorage.getItem(`top-spotify-state`);
|
||||
if (LS_state = params.get(`state`)) {
|
||||
console.info(`State compare success`)
|
||||
this.authed = true;
|
||||
return true
|
||||
}
|
||||
console.error(`State compare failed`)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
let error = (new URLSearchParams(window.location.search)).get(`error`)
|
||||
|
||||
// Authorization failed, error to the user
|
||||
if (error !== null) {
|
||||
this.error.auth = `Authentication failed or was cancelled, please try again.`;
|
||||
window.location.hash = ``;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
57
js/data.js
Normal file
57
js/data.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
function fetch_data () {
|
||||
let url = `${this.api_base}/me/top/${this.type.toLowerCase()}`;
|
||||
|
||||
let limit = parseInt(this.count);
|
||||
if (!limit) { limit = 10; };
|
||||
|
||||
url += `?limit=${limit}&time_range=${this.duration}`;
|
||||
|
||||
axios.get(
|
||||
url,
|
||||
{ headers: { Authorization: `Bearer ${this.get_token()}` } }
|
||||
).then((response) => {
|
||||
this.data.artists = [];
|
||||
this.data.tracks = [];
|
||||
this.error.main = ``;
|
||||
switch (this.type) {
|
||||
case `Tracks`:
|
||||
for (var track of response.data.items) {
|
||||
this.data.tracks.push({
|
||||
name: track.name,
|
||||
popularity: track.popularity,
|
||||
artists: track.artists,
|
||||
link: track.external_urls.spotify,
|
||||
duration: track.duration_ms,
|
||||
locality: track.is_local,
|
||||
id: track.uri,
|
||||
album: {
|
||||
name: track.album.name,
|
||||
image: track.album.images[1],
|
||||
link: track.album.external_urls.spotify
|
||||
}
|
||||
});
|
||||
};
|
||||
break;
|
||||
|
||||
case `Artists`:
|
||||
for (var artist of response.data.items) {
|
||||
this.data.artists.push({
|
||||
name: artist.name,
|
||||
id: artist.id,
|
||||
popularity: artist.popularity,
|
||||
follower_count: artist.followers.total,
|
||||
image: artist.images[1],
|
||||
genres: artist.genres,
|
||||
link: artist.external_urls.spotify
|
||||
});
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
this.error.main = `TypeError: ${this.type} is not a supported category`;
|
||||
break
|
||||
};
|
||||
}).catch((err) => {
|
||||
this.error.main = `${err.name}: ${err.message}`
|
||||
})
|
||||
}
|
||||
19
js/text_computation.js
Normal file
19
js/text_computation.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
function get_button_text () {
|
||||
if (this.count === ``) {
|
||||
return this.type;
|
||||
}
|
||||
else if (this.count === `1`) {
|
||||
this.error.main = ``;
|
||||
return this.type.slice(0,-1);
|
||||
}
|
||||
else if (this.count <= 0) {
|
||||
this.error.main = `Cannot get 0 or fewer ${this.type.toLowerCase()}`;
|
||||
return false;
|
||||
}
|
||||
else if (this.count > 50) {
|
||||
this.error.main = `Cannot get more than 50 ${this.type.toLowerCase()}`;
|
||||
return false;
|
||||
}
|
||||
this.error.main = ``;
|
||||
return this.type;
|
||||
};
|
||||
293
style.css
Normal file
293
style.css
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
:root {
|
||||
--spotify-green: #1DB954;
|
||||
--spotify-white: #FFFFFF;
|
||||
--spotify-black: #000000;
|
||||
|
||||
--accent1: #7289da;
|
||||
--accent2: #00aa00;
|
||||
|
||||
--error: #ff0000;
|
||||
|
||||
--background: #23272A;
|
||||
--background-text: var(--spotify-white);
|
||||
|
||||
--card-colour: #2C2F33;
|
||||
--card-text: #ffffff80;
|
||||
|
||||
--on-card-colour: #4c4c4c;
|
||||
--on-card-text: var(--spotify-green);
|
||||
|
||||
--fonts: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
font-family: var(--fonts);
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
|
||||
p {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: var(--spotify-black);
|
||||
color: var(--spotify-white);
|
||||
padding: 15px;
|
||||
border-style: none;
|
||||
border-radius: 7px;
|
||||
font-family: var(--fonts);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background-color: var(--spotify-green);
|
||||
border-style: none;
|
||||
border-radius: 50px;
|
||||
font-size: larger;
|
||||
font-family: var(--fonts);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
background-color: var(--spotify-black);
|
||||
color: var(--spotify-white);
|
||||
padding: 15px;
|
||||
border-style: none;
|
||||
border-radius: 7px;
|
||||
font-family: var(--fonts);
|
||||
outline: none;
|
||||
}
|
||||
input[type=number]:active {
|
||||
border-color: var(--spotify-green);
|
||||
}
|
||||
|
||||
|
||||
div.body {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
width: 95%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
|
||||
[v-cloak], .hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#login {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--spotify-green);
|
||||
}
|
||||
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
background-color: var(--card-colour);
|
||||
color: var(--card-text);
|
||||
margin: 10px auto;
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.flex-row > div {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 90%;
|
||||
background-color: var(--card-colour);
|
||||
color: var(--card-text);
|
||||
margin: 10px auto;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
border-style: solid;
|
||||
border-radius: 5px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: inline-block;
|
||||
color: var(--card-text);
|
||||
padding: 20px;
|
||||
margin: 5px;
|
||||
background-color: var(--card-colour);
|
||||
border-radius: 7px;
|
||||
font-size: large;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
color: var(--spotify-white);
|
||||
}
|
||||
|
||||
.profile-picture {
|
||||
--profile-pic-width: 50px;
|
||||
width: var(--profile-pic-width);
|
||||
height: var(--profile-pic-width);
|
||||
vertical-align: middle;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip Styling */
|
||||
.tooltip {
|
||||
display: none !important;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-inner {
|
||||
background: var(--spotify-black);
|
||||
color: var(--spotify-green);
|
||||
border-radius: 16px;
|
||||
padding: 5px 10px 4px;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 5px;
|
||||
border-color: black;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="top"] {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="top"] .tooltip-arrow {
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
bottom: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="bottom"] {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="bottom"] .tooltip-arrow {
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
top: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="right"] {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="right"] .tooltip-arrow {
|
||||
border-width: 5px 5px 5px 0;
|
||||
border-left-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
left: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="left"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tooltip[x-placement^="left"] .tooltip-arrow {
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-top-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
right: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tooltip.popover .popover-inner {
|
||||
background: #f9f9f9;
|
||||
color: black;
|
||||
padding: 24px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 30px rgba(black, .1);
|
||||
}
|
||||
|
||||
.tooltip.popover .popover-arrow {
|
||||
border-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.tooltip[aria-hidden='true'] {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity .15s, visibility .15s;
|
||||
}
|
||||
|
||||
.tooltip[aria-hidden='false'] {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
/* End of Tooltip */
|
||||
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
.tooltip {
|
||||
display: block !important;
|
||||
}
|
||||
div.body {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 90%;
|
||||
}
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.card {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue