From af75bbc522d0a019c7216e04eb823646e6cd62d6 Mon Sep 17 00:00:00 2001 From: Tyler-A Date: Sun, 5 Jul 2020 18:14:59 -0600 Subject: [PATCH] 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