From af75bbc522d0a019c7216e04eb823646e6cd62d6 Mon Sep 17 00:00:00 2001 From: Tyler-A Date: Sun, 5 Jul 2020 18:14:59 -0600 Subject: [PATCH 1/4] Make app function. --- app.js | 71 ++++++++++ components/artist.html | 14 ++ components/artist.js | 24 ++++ components/icons.js | 0 components/track.html | 20 +++ components/track.js | 63 +++++++++ css/artist.css | 84 ++++++++++++ css/track.css | 84 ++++++++++++ expired_tokens | 1 + index.html | 82 ++++++++++++ js/auth.js | 53 ++++++++ js/data.js | 57 ++++++++ js/text_computation.js | 19 +++ style.css | 293 +++++++++++++++++++++++++++++++++++++++++ 14 files changed, 865 insertions(+) create mode 100644 app.js create mode 100644 components/artist.html create mode 100644 components/artist.js create mode 100644 components/icons.js create mode 100644 components/track.html create mode 100644 components/track.js create mode 100644 css/artist.css create mode 100644 css/track.css create mode 100644 expired_tokens create mode 100644 index.html create mode 100644 js/auth.js create mode 100644 js/data.js create mode 100644 js/text_computation.js create mode 100644 style.css diff --git a/app.js b/app.js new file mode 100644 index 0000000..bf8c45c --- /dev/null +++ b/app.js @@ -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, + } +}) \ No newline at end of file diff --git a/components/artist.html b/components/artist.html new file mode 100644 index 0000000..0a09fd8 --- /dev/null +++ b/components/artist.html @@ -0,0 +1,14 @@ +
+
+ +
+
+ + {{artist.name}} + +
+ {{artist.genres.join(", ")}} +
+
{{artist.popularity}}
+
{{artist.follower_count}}
+
\ No newline at end of file diff --git a/components/artist.js b/components/artist.js new file mode 100644 index 0000000..7fcd7ca --- /dev/null +++ b/components/artist.js @@ -0,0 +1,24 @@ +Vue.component( + `artist`, + { + props: [ `artist` ], + data: function () { + return {}; + }, + computed: {}, + template: `
+
+ +
+
+ + {{artist.name}} + +
+ {{artist.genres.join(", ")}} +
+
{{artist.popularity}}
+
{{artist.follower_count}}
+
` + } +) \ No newline at end of file diff --git a/components/icons.js b/components/icons.js new file mode 100644 index 0000000..e69de29 diff --git a/components/track.html b/components/track.html new file mode 100644 index 0000000..1ff52fe --- /dev/null +++ b/components/track.html @@ -0,0 +1,20 @@ +
+
+ + + +
+
+ + {{track.name}} + + + {{track.name}} + +
+ +
+
+
{{track.popularity}}
+
{{duration}}
+
\ No newline at end of file diff --git a/components/track.js b/components/track.js new file mode 100644 index 0000000..ed2dd9f --- /dev/null +++ b/components/track.js @@ -0,0 +1,63 @@ +Vue.component( + `track-card`, + { + props: [ `track` ], + data: function () { + return { + popularity_tooltip: `Popularity.\nClick for more information.` + } + }, + template: `
+
+ + + +
+
+ + {{track.name}} + + + {{track.name}} + +
+ +
+
+
{{track.popularity}}
+
{{duration}}
+
`, + 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( + `${artist.name}` + ) + } + return artists.join(`, `) + }, + } + } +); \ No newline at end of file diff --git a/css/artist.css b/css/artist.css new file mode 100644 index 0000000..ffa6d93 --- /dev/null +++ b/css/artist.css @@ -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; + } +} \ No newline at end of file diff --git a/css/track.css b/css/track.css new file mode 100644 index 0000000..ae07fd1 --- /dev/null +++ b/css/track.css @@ -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; + } +} \ No newline at end of file diff --git a/expired_tokens b/expired_tokens new file mode 100644 index 0000000..892febf --- /dev/null +++ b/expired_tokens @@ -0,0 +1 @@ +BQCai7XSoBzX6DhHscdWeWOeg_0J6KC6-mL2YFDzvQsWmlGUFMdXCTFji0TpxpSX-bak1GretW99OyyvXAY4xiECbou6H2_wDq8Zq-Yzr9UUeph0zF57BZ3nZjLA0vMDu3AXzGMQqPml9POL \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3357e72 --- /dev/null +++ b/index.html @@ -0,0 +1,82 @@ + + + + + + Top Lists For Spotify + + + + + + + + + + + + + + + + + + + +
+
+
+ + + +

{{error.auth}}

+

{{auth.alert}}

+
+
+
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
{{error.main}}
+
+ +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/js/auth.js b/js/auth.js new file mode 100644 index 0000000..24d8bdc --- /dev/null +++ b/js/auth.js @@ -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; + } +} \ No newline at end of file diff --git a/js/data.js b/js/data.js new file mode 100644 index 0000000..aa0ed14 --- /dev/null +++ b/js/data.js @@ -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}` + }) +} \ No newline at end of file diff --git a/js/text_computation.js b/js/text_computation.js new file mode 100644 index 0000000..c62cc63 --- /dev/null +++ b/js/text_computation.js @@ -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; +}; \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..8ab3d75 --- /dev/null +++ b/style.css @@ -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%; + } +} \ No newline at end of file From 642c30653dcef84c5c2e4d96d44455dff4b4b6fd Mon Sep 17 00:00:00 2001 From: Tyler-A Date: Sun, 5 Jul 2020 18:15:56 -0600 Subject: [PATCH 2/4] Remove expired tokens file --- expired_tokens | 1 - 1 file changed, 1 deletion(-) delete mode 100644 expired_tokens diff --git a/expired_tokens b/expired_tokens deleted file mode 100644 index 892febf..0000000 --- a/expired_tokens +++ /dev/null @@ -1 +0,0 @@ -BQCai7XSoBzX6DhHscdWeWOeg_0J6KC6-mL2YFDzvQsWmlGUFMdXCTFji0TpxpSX-bak1GretW99OyyvXAY4xiECbou6H2_wDq8Zq-Yzr9UUeph0zF57BZ3nZjLA0vMDu3AXzGMQqPml9POL \ No newline at end of file From 43964e38df16f61bfb9cb66ccb3be3290f817c89 Mon Sep 17 00:00:00 2001 From: Tyler-A Date: Sun, 5 Jul 2020 18:16:22 -0600 Subject: [PATCH 3/4] Add gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5c835b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +expired_tokens \ No newline at end of file From fb58e20f7a8e625ecd5ca560b0e7d247ae2c7cbc Mon Sep 17 00:00:00 2001 From: Tyler-A Date: Mon, 6 Jul 2020 00:15:58 -0600 Subject: [PATCH 4/4] Make missing artist profile pictures a default & remove click for more info on popularity --- components/artist.html | 9 +++++++-- components/artist.js | 14 ++++++++------ components/icons.js | 7 +++++++ components/track.js | 2 +- css/artist.css | 15 ++++++++++++++- index.html | 1 + js/text_computation.js | 2 +- 7 files changed, 39 insertions(+), 11 deletions(-) diff --git a/components/artist.html b/components/artist.html index 0a09fd8..8272f7a 100644 --- a/components/artist.html +++ b/components/artist.html @@ -1,6 +1,11 @@
-
- +
+ +
+
+
+ +
diff --git a/components/artist.js b/components/artist.js index 7fcd7ca..722cc27 100644 --- a/components/artist.js +++ b/components/artist.js @@ -2,13 +2,15 @@ Vue.component( `artist`, { props: [ `artist` ], - data: function () { - return {}; - }, computed: {}, template: `
-
- +
+ +
+
+
+ +
@@ -20,5 +22,5 @@ Vue.component(
{{artist.popularity}}
{{artist.follower_count}}
` - } +} ) \ No newline at end of file diff --git a/components/icons.js b/components/icons.js index e69de29..a7c9751 100644 --- a/components/icons.js +++ b/components/icons.js @@ -0,0 +1,7 @@ +Vue.component( + `music-note`, + { + props: [ `colour` ], + template: `` + } +); \ No newline at end of file diff --git a/components/track.js b/components/track.js index ed2dd9f..3d14916 100644 --- a/components/track.js +++ b/components/track.js @@ -4,7 +4,7 @@ Vue.component( props: [ `track` ], data: function () { return { - popularity_tooltip: `Popularity.\nClick for more information.` + popularity_tooltip: `Popularity` } }, template: `
diff --git a/css/artist.css b/css/artist.css index ffa6d93..76cb5d9 100644 --- a/css/artist.css +++ b/css/artist.css @@ -17,11 +17,24 @@ div.artist { div.artist > div.profile_pic { text-align: center; } -div.artist > div.profile_pic img { +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; diff --git a/index.html b/index.html index 3357e72..d6d4df4 100644 --- a/index.html +++ b/index.html @@ -16,6 +16,7 @@ + diff --git a/js/text_computation.js b/js/text_computation.js index c62cc63..0b10959 100644 --- a/js/text_computation.js +++ b/js/text_computation.js @@ -13,7 +13,7 @@ function get_button_text () { else if (this.count > 50) { this.error.main = `Cannot get more than 50 ${this.type.toLowerCase()}`; return false; - } + }; this.error.main = ``; return this.type; }; \ No newline at end of file