commit
e248b0be82
14 changed files with 893 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
expired_tokens
|
||||||
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
19
components/artist.html
Normal file
19
components/artist.html
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<div class="artist">
|
||||||
|
<div class="image profile_pic" v-if="artist.image">
|
||||||
|
<img :src="artist.image.url" :alt="artist.name + 's profile picture'">
|
||||||
|
</div>
|
||||||
|
<div class="image profile_pic" v-else>
|
||||||
|
<div class="missing-circle">
|
||||||
|
<music-note colour="#1DB954"></music-note>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
26
components/artist.js
Normal file
26
components/artist.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
Vue.component(
|
||||||
|
`artist`,
|
||||||
|
{
|
||||||
|
props: [ `artist` ],
|
||||||
|
computed: {},
|
||||||
|
template: `<div class="artist">
|
||||||
|
<div class="image profile_pic" v-if="artist.image">
|
||||||
|
<img :src="artist.image.url" :alt="artist.name + 's profile picture'">
|
||||||
|
</div>
|
||||||
|
<div class="image profile_pic" v-else>
|
||||||
|
<div class="missing-circle">
|
||||||
|
<music-note colour="#1DB954"></music-note>
|
||||||
|
</div>
|
||||||
|
</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>`
|
||||||
|
}
|
||||||
|
)
|
||||||
7
components/icons.js
Normal file
7
components/icons.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Vue.component(
|
||||||
|
`music-note`,
|
||||||
|
{
|
||||||
|
props: [ `colour` ],
|
||||||
|
template: `<svg width="72" height="72" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="24" height="24" fill="none" rx="0" ry="0"></rect><path fill-rule="evenodd" clip-rule="evenodd" d="M17.4911 2.11667L21.4511 4.09667C21.9411 4.34667 22.1411 4.93667 21.8911 5.43667C21.6411 5.93667 21.0411 6.13667 20.5511 5.88667L18.0011 4.61667L17.9911 16.4367C17.9911 16.4467 17.9936 16.4567 17.9961 16.4667C17.9986 16.4767 18.0011 16.4867 18.0011 16.4967C18.0011 18.4267 16.4311 19.9967 14.5011 19.9967C12.5711 19.9967 11.0011 18.4267 11.0011 16.4967C11.0011 14.5667 12.5711 12.9967 14.5011 12.9967C15.0411 12.9967 15.5411 13.1267 16.0011 13.3467L16.0111 3.10667C15.9911 2.99667 16.0011 2.87667 16.0311 2.75667C16.1711 2.14667 16.9411 1.80667 17.4911 2.11667ZM12.8911 5.43664C12.6511 5.93664 12.0511 6.13664 11.5511 5.88664L9.0011 4.61664L8.9911 18.4366C8.9911 18.4466 8.9936 18.4566 8.9961 18.4666C8.9986 18.4766 9.0011 18.4866 9.0011 18.4966C9.0011 20.4266 7.4311 21.9966 5.5011 21.9966C3.5711 21.9966 2.0011 20.4266 2.0011 18.4966C2.0011 16.5666 3.5711 14.9966 5.5011 14.9966C6.0411 14.9966 6.5411 15.1266 7.0011 15.3466L7.0111 3.10664C6.9911 2.99664 7.0011 2.87664 7.0311 2.76664C7.1711 2.14664 7.9411 1.81664 8.4911 2.12664L12.4411 4.09664C12.9411 4.34664 13.1411 4.94664 12.8911 5.43664Z" :fill="colour || '#ffffff'"></path></svg>`
|
||||||
|
}
|
||||||
|
);
|
||||||
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`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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(`, `)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
97
css/artist.css
Normal file
97
css/artist.css
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
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.profile_pic > div.missing-circle {
|
||||||
|
background-color: #3a3a3aaa;
|
||||||
|
border-radius: 100px;
|
||||||
|
display: flex;
|
||||||
|
height: 200px;
|
||||||
|
width: 200px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
index.html
Normal file
83
index.html
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<!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="./components/icons.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