0
0
Fork 0

Make app function.

This commit is contained in:
Tyler-A 2020-07-05 18:14:59 -06:00
parent e4c81beefe
commit af75bbc522
14 changed files with 865 additions and 0 deletions

71
app.js Normal file
View 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
View 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
View 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
View file

20
components/track.html Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
BQCai7XSoBzX6DhHscdWeWOeg_0J6KC6-mL2YFDzvQsWmlGUFMdXCTFji0TpxpSX-bak1GretW99OyyvXAY4xiECbou6H2_wDq8Zq-Yzr9UUeph0zF57BZ3nZjLA0vMDu3AXzGMQqPml9POL

82
index.html Normal file
View 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
View 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
View 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
View 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
View 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%;
}
}