diff --git a/.env b/.env index 24b5f3f..1a27343 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ VITE_CLIENT_ID="" VITE_CLIENT_SECRET="" -VITE_CALLBACK_URL="http://localhost:5173/" +VITE_CALLBACK_URL="http://localhost:5173/callback" diff --git a/README.md b/README.md index e045c73..9544e8e 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,10 @@ Make sure you go through this checklist before submitting your project to Moodle - [Animate.css](https://animate.style/) - Just-add-water CSS animations - [Spotify-web-api-js](https://www.npmjs.com/package/spotify-web-api-js) +## Tutorials & Reading material + +- [storing tokens](https://blog.ropnop.com/storing-tokens-in-browser/) + ## Authors - Ali Nough (@AliNough) diff --git a/callback.html b/callback.html new file mode 100644 index 0000000..a46d040 --- /dev/null +++ b/callback.html @@ -0,0 +1,31 @@ + + + + + + + Bragi Mix + + + + + + +
+
+

You should be automatically redirect to the home page

+

If not please click the button below

+ + Go Back +
+
+ + + + diff --git a/components/Header.js b/components/Header.js new file mode 100644 index 0000000..7956b52 --- /dev/null +++ b/components/Header.js @@ -0,0 +1,41 @@ +class Header extends HTMLElement { + constructor() { + super(); + + this.attachShadow({ mode: "open" }); + this.shadowRoot.innerHTML = ` + +
+

+
+ + + +
+
+ `; + + const currentUrl = window.location.pathname; + + const links = this.shadowRoot.querySelectorAll("a"); + + links.forEach((link) => { + if (currentUrl.includes(link.pathname) && link.pathname !== "/") { + link.classList.add("is-active"); + return; + } + }); + } +} + +customElements.define("custom-header", Header); diff --git a/counter.js b/counter.js deleted file mode 100644 index 881e2d7..0000000 --- a/counter.js +++ /dev/null @@ -1,9 +0,0 @@ -export function setupCounter(element) { - let counter = 0 - const setCounter = (count) => { - counter = count - element.innerHTML = `count is ${counter}` - } - element.addEventListener('click', () => setCounter(counter + 1)) - setCounter(0) -} diff --git a/css/form.css b/css/form.css index 21ad9a2..6a78af6 100644 --- a/css/form.css +++ b/css/form.css @@ -6,7 +6,8 @@ .c-form { display: flex; + flex-direction: column; + width: 90%; gap: var(--size-fluid-1); padding: var(--size-fluid-1); - flex-direction: column; } diff --git a/css/layout.css b/css/layout.css index 745e8b3..964caef 100644 --- a/css/layout.css +++ b/css/layout.css @@ -53,6 +53,10 @@ body { } .l-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; width: 100%; max-width: 1000px; margin: 0 auto; diff --git a/css/ui.css b/css/ui.css index 1b3c733..2a0935b 100644 --- a/css/ui.css +++ b/css/ui.css @@ -24,3 +24,37 @@ box-shadow: var(--shadow-3); margin: var(--size-2) 0; } + +.c-nav-controls { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.c-song-preview-window { + width: 100%; + height: 80%; + min-height: 20vh; + margin-top: 1em; +} + +.c-song-preview-window html { + background-image: url(https://i.scdn.co/image/ab67616d0000b273c8769c2ca401307f6122b5c1); + background-color: blue; +} + +.audio-player { + position: relative; + width: 100%; +} +audio.audio-player::-webkit-media-controls-panel { + width: 10px; + border-radius: 100%; + background-color: rgb(58, 58, 67); +} + +audio.audio-player::-webkit-media-controls-play-button { + border-radius: 100%; + background-color: rgb(123, 169, 169); +} diff --git a/css/utilities.css b/css/utilities.css index dfe5b06..2ac9386 100644 --- a/css/utilities.css +++ b/css/utilities.css @@ -16,3 +16,8 @@ .is-visible { opacity: 1; } + +.u-row { + display: flex; + flex-direction: row; +} diff --git a/index.html b/index.html index 8292e59..9d53440 100644 --- a/index.html +++ b/index.html @@ -4,33 +4,35 @@ - Vite App - + Bragi Mix + + +
-
-
-

Login into your Spotify

- -
-
-
+
diff --git a/js/app.js b/js/app.js deleted file mode 100644 index 8b93a3b..0000000 --- a/js/app.js +++ /dev/null @@ -1,53 +0,0 @@ -/* -============================================ -Constants -@example: https://github.com/S3ak/fed-javascript1-api-calls/blob/main/examples/games.html#L66 -============================================ -*/ - -// TODO: Get DOM elements from the DOM - -/* -============================================ -DOM manipulation -@example: https://github.com/S3ak/fed-javascript1-api-calls/blob/main/examples/games.html#L89 -============================================ -*/ - -// TODO: Fetch and Render the list to the DOM - -// TODO: Create event listeners for the filters and the search - -/** - * TODO: Create an event listener to sort the list. - * @example https://github.com/S3ak/fed-javascript1-api-calls/blob/main/examples/search-form.html#L91 - */ - -/* -============================================ -Data fectching -@example: https://github.com/S3ak/fed-javascript1-api-calls/blob/main/examples/games.html#L104 -============================================ -*/ - -// TODO: Fetch an array of objects from the API - -/* -============================================ -Helper functions -https://github.com/S3ak/fed-javascript1-api-calls/blob/main/examples/games.html#L154 -============================================ -*/ - -/** - * TODO: Create a function to filter the list of item. - * @example https://github.com/S3ak/fed-javascript1-api-calls/blob/main/examples/search-form.html#L135 - * @param {item} item The object with properties from the fetched JSON data. - * @param {searchTerm} searchTerm The string used to check if the object title contains it. - */ - -/** - * TODO: Create a function to create a DOM element. - * @example https://github.com/S3ak/fed-javascript1-api-calls/blob/main/src/js/detail.js#L36 - * @param {item} item The object with properties from the fetched JSON data. - */ diff --git a/js/callback.js b/js/callback.js new file mode 100644 index 0000000..a86c11d --- /dev/null +++ b/js/callback.js @@ -0,0 +1,10 @@ +import { manageUserSessssion } from "./session.js"; + +/* +============================================ +Callback page specfic code +============================================ +*/ + +manageUserSessssion(); +window.location.replace("/index.html"); diff --git a/js/constants.js b/js/constants.js new file mode 100644 index 0000000..26312b1 --- /dev/null +++ b/js/constants.js @@ -0,0 +1,128 @@ +export const GENRES = [ + "acoustic", + "afrobeat", + "alt-rock", + "alternative", + "ambient", + "anime", + "black-metal", + "bluegrass", + "blues", + "bossanova", + "brazil", + "breakbeat", + "british", + "cantopop", + "chicago-house", + "children", + "chill", + "classical", + "club", + "comedy", + "country", + "dance", + "dancehall", + "death-metal", + "deep-house", + "detroit-techno", + "disco", + "disney", + "drum-and-bass", + "dub", + "dubstep", + "edm", + "electro", + "electronic", + "emo", + "folk", + "forro", + "french", + "funk", + "garage", + "german", + "gospel", + "goth", + "grindcore", + "groove", + "grunge", + "guitar", + "happy", + "hard-rock", + "hardcore", + "hardstyle", + "heavy-metal", + "hip-hop", + "holidays", + "honky-tonk", + "house", + "idm", + "indian", + "indie", + "indie-pop", + "industrial", + "iranian", + "j-dance", + "j-idol", + "j-pop", + "j-rock", + "jazz", + "k-pop", + "kids", + "latin", + "latino", + "malay", + "mandopop", + "metal", + "metal-misc", + "metalcore", + "minimal-techno", + "movies", + "mpb", + "new-age", + "new-release", + "opera", + "pagode", + "party", + "philippines-opm", + "piano", + "pop", + "pop-film", + "post-dubstep", + "power-pop", + "progressive-house", + "psych-rock", + "punk", + "punk-rock", + "r-n-b", + "rainy-day", + "reggae", + "reggaeton", + "road-trip", + "rock", + "rock-n-roll", + "rockabilly", + "romance", + "sad", + "salsa", + "samba", + "sertanejo", + "show-tunes", + "singer-songwriter", + "ska", + "sleep", + "songwriter", + "soul", + "soundtracks", + "spanish", + "study", + "summer", + "swedish", + "synth-pop", + "tango", + "techno", + "trance", + "trip-hop", + "turkish", + "work-out", + "world-music", +]; diff --git a/js/helpers.js b/js/helpers.js new file mode 100644 index 0000000..73acc66 --- /dev/null +++ b/js/helpers.js @@ -0,0 +1,31 @@ +/* +============================================ +Helper functions +https://github.com/S3ak/fed-javascript1-api-calls/blob/main/examples/games.html#L154 +============================================ +*/ + +// TODO: Create a function that renders the playlists +export function renderPlaylist({ title = "No title" }) { + return ` +
+

${title}

+
+ `; +} + +export function hideProtectedContent(isLoggedIn) { + const protectedContent = document.querySelectorAll("[data-is-protected]"); + + if (isLoggedIn) { + protectedContent.forEach((content) => { + content.classList.remove("is-hidden"); + }); + + return; + } + + protectedContent.forEach((content) => { + content.classList.add("is-hidden"); + }); +} diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..33389a0 --- /dev/null +++ b/js/index.js @@ -0,0 +1,27 @@ +import { renderPlaylist } from "./helpers.js"; + +import s from "../main.js"; + +const playlistContainerEl = document.querySelector("#js-playlist-list"); + +document.querySelector("#js-seach-form").addEventListener("submit", (event) => { + event.preventDefault(); + + const genre = event.target.querySelector("#genre").value; + s.searchPlaylists(genre) + .then((data) => { + const playlists = data.playlists.items; + console.log(playlists); + + playlistContainerEl.innerHTML = ""; + + playlists.forEach((playlist) => { + playlistContainerEl.innerHTML += renderPlaylist({ + title: playlist.name, + }); + }); + }) + .catch((error) => { + console.log(error); + }); +}); diff --git a/js/login.js b/js/login.js new file mode 100644 index 0000000..80886f6 --- /dev/null +++ b/js/login.js @@ -0,0 +1,11 @@ +import { loginSpotifyUser } from "./session.js"; + +/* +============================================ +Login page specfic code +============================================ +*/ + +const loginBtnEl = document.querySelector("#js-login-btn"); + +loginBtnEl.addEventListener("click", loginSpotifyUser); diff --git a/js/randomize.js b/js/randomize.js index 627b600..3bb5308 100644 --- a/js/randomize.js +++ b/js/randomize.js @@ -1,40 +1,96 @@ -import Spotify from "spotify-web-api-js"; -// import Spotify from "../node_modules/spotify-web-api-js/src/typings/spotify-web-api"; +import s from "../main.js"; + +import { GENRES } from "./constants.js"; const generateBtn = document.querySelector("#js-gnrt-btn"); const resultHolder = document.querySelector("#js-res-holder"); -const yearInput = document.querySelector("#js-yr-input"); -console.log(resultHolder); +// const resultHolder2 = document.querySelector("#js-res-holder-2"); +// const songPreview = document.querySelector("#js-iframe"); +const countryInp = document.querySelector("#js-country-input"); +const yearCalInput = document.querySelector("#js-yr-cal-input"); +const genreInput = document.querySelector("#js-genre-input"); +const genresList = document.querySelector("#js-genres-list"); +// const srchTypeInp = document.querySelector("#js-search-type-input"); + +let selectedYear = "1950"; +let selectedGenre = "jazz"; +// let market = "NO"; +let srchQ = `year:${selectedYear} genre:${selectedGenre}`; +// let srchQ = `isrc:${market} year:${selectedYear} genre:${selectedGenre}`; + +genresList.innerHTML = GENRES.map( + (genre) => `` +); -generateBtn.addEventListener("click", () => { - randomizer(yearInput.value); +yearCalInput.addEventListener("change", (event) => { + selectedYear = event.target.value.substring(0, 4); + srchQ = `year:${selectedYear} genre:${selectedGenre}`; }); -const accessToken = localStorage.getItem("access_token"); -console.log(accessToken); +genreInput.addEventListener("change", (event) => { + selectedGenre = event.target.value; + srchQ = `isrc:JP year:${selectedYear} genre:${selectedGenre}`; +}); -const dataBase = new Spotify(); -console.log(dataBase); -// const accessToken = new URLSearchParams( -// window.location.hash.replace("#", "") -// ).get("access_token"); +// countryInp.addEventListener("change", (event) => { +// market = event.target.value; +// srchQ = `isrc:${market} year:${selectedYear} genre:${selectedGenre}`; +// }); -// First get a list of songs from the country (Norway). -// From the list pick a random song and get the id -// get the song URI using the Id https://jmperezperez.com/spotify-web-api-js/#src-spotify-web-api.js-constr.prototype.gettrack +generateBtn.addEventListener("click", getSong); + +const resultLmt = 50; +// const searchType = ["track"]; +const optns = { + limit: resultLmt, + market: countryInp.value, + safeSearch: true, +}; + +function getSong() { + const randNum = Math.floor(Math.random() * (resultLmt - 1)); + + s.searchTracks(srchQ, optns).then((data) => { + console.log(data); -function randomizer(yrInput = 0) { - // randNum>> a random number between 0 and 19. replace 19 with response limit - resultHolder.innerHTML = ""; - const randNum = Math.floor(Math.random() * 20); - console.log(randNum); - // Problem: the URI is unique for each song, so rand number wont work here. - // we need to first get a catalog back and then use the rand number. but how? - // dataBase.getCategories().then((tracks) => { - // console.log(tracks); + if (data.tracks.items.length === 0) { + resultHolder.innerHTML = ` +

There were no ${selectedGenre} in ${selectedYear}'s

+

Please select another genre or year

+ `; + return; + } + + const item = data.tracks.items[randNum]; + const song = item.preview_url; + console.log(song); + + console.log(item.album.images[0].url); + + resultHolder.innerHTML = ` + pix + +

${item.name} - ${item.artists[0].name}

+
ID: ${randNum}
+ Open on Spotify + `; + }); + + // s.search(srchQ, searchType, optns).then((data) => { + // console.log(data); + // const item = data.tracks.items[randNum]; + + // resultHolder2.innerHTML = ` + //

${item.name} - ${item.artists[0].name}

+ //
ID: ${randNum}
+ // Open on Spotify + // `; // }); - resultHolder.innerHTML = ` -

rand.nr: ${randNum}. Year input: ${yrInput}

- `; - console.log(resultHolder); } + +// First get a list of songs from the country (Norway). +// From the list pick a random song and get the id +// get the song URI using the Id https://jmperezperez.com/spotify-web-api-js/#src-spotify-web-api.js-constr.prototype.gettrack +// https://api.spotify.com/v1/search?q=year:YEAR&type=track&limit=50&offset=OFFSET diff --git a/js/session.js b/js/session.js new file mode 100644 index 0000000..3848ff0 --- /dev/null +++ b/js/session.js @@ -0,0 +1,60 @@ +import { hideProtectedContent } from "./helpers.js"; + +const ACCESS_TOKEN_KEY = "access_token"; + +export const getAccessToken = () => { + let token = null; + + const cachedAccessToken = localStorage.getItem(ACCESS_TOKEN_KEY); + + const currentSessionToken = new URLSearchParams( + window.location.hash.replace("#", "") + ).get(ACCESS_TOKEN_KEY); + + if (cachedAccessToken) { + token = cachedAccessToken; + } + + if (currentSessionToken) { + localStorage.setItem(ACCESS_TOKEN_KEY, currentSessionToken); + window.location.hash = ""; + token = currentSessionToken; + } + + return token; +}; + +export const checkIfUserIsLoggedIn = () => !!getAccessToken(); + +export const manageUserSessssion = () => { + const userIsLoggedIn = checkIfUserIsLoggedIn(); + + hideProtectedContent(userIsLoggedIn); + + if (!userIsLoggedIn) { + window.location.href = "/login.html"; + } +}; + +export async function loginSpotifyUser() { + // NOTE: We need to request authorization from the user to access their data. + const authUrl = new URL("https://accounts.spotify.com/authorize"); + + authUrl.searchParams.set("response_type", "token"); + authUrl.searchParams.set("client_id", import.meta.env.VITE_CLIENT_ID); + authUrl.searchParams.set("redirect_uri", import.meta.env.VITE_CALLBACK_URL); + authUrl.searchParams.set("scope", "user-read-private user-read-email"); + // REMOVE THIS IN PRODUCTION + authUrl.searchParams.set("show_dialog", "true"); + + try { + window.location.replace(authUrl); + } catch (error) { + console.log(error); + } +} + +export function logOut() { + localStorage.removeItem(ACCESS_TOKEN_KEY); + window.location.replace("/login.html"); +} diff --git a/login.html b/login.html new file mode 100644 index 0000000..41bd549 --- /dev/null +++ b/login.html @@ -0,0 +1,29 @@ + + + + + + + Bragi Mix | Login + + + + + + +
+
+

Login into your Spotify

+ +
+
+ +
+
Made by Alex, Ali & Mo
+
+ + diff --git a/main.js b/main.js index 70fc0ee..ebd54c3 100644 --- a/main.js +++ b/main.js @@ -1,95 +1,34 @@ import Spotify from "spotify-web-api-js"; -const playlistContainerEl = document.querySelector("#js-playlist-list"); -const loginContainerEl = document.querySelector("#js-login-holder"); -const randNavContainerEl = document.querySelector("#js-rand-nav-holder"); +import { + getAccessToken, + loginSpotifyUser, + logOut, + manageUserSessssion, +} from "./js/session.js"; -// NOTE: We use this lib to create a Spotify instance. +const loginBtnEl = document.querySelector("#js-login-btn"); +const logOutBtnEl = document.querySelector("#js-logout-btn"); + +// NOTE: We use this lib to create a Spotify instance. It makes it easier to work with the Spotify API. const s = new Spotify(); +manageUserSessssion(); + // NOTE: We get the access token for the client from the callback URI issued when user logs into spotify. -let cachedAccessToken = localStorage.getItem("access_token"); -const accessToken = - cachedAccessToken ?? - new URLSearchParams(window.location.hash.replace("#", "")).get( - "access_token" - ); +// @alias accessToken +const isLoggedIn = getAccessToken(); -if (accessToken) { - cachedAccessToken = localStorage.setItem("access_token", accessToken); +if (isLoggedIn) { + s.setAccessToken(isLoggedIn); + loginBtnEl.classList.add("is-hidden"); - s.setAccessToken(accessToken); - loginContainerEl.classList.add("is-hidden"); - randNavContainerEl.innerHTML = ` - randomizer - playlists - - `; + logOutBtnEl.addEventListener("click", logOut); } else { - loginContainerEl.classList.remove("is-hidden"); -} - -const randBtn = document.querySelector("#js-rand-btn"); -console.log(randBtn); - -// randBtn.addEventListener("click", () => { -// randomizer(); -// }); - -// TODO: Add event listener to login button only if user is not logged in -document.querySelector("#js-login-btn").addEventListener("click", () => { - // ???calling a function with a arg that is never used. func does not take an arg??? - loginSpotifyUser(s.setAccessToken); -}); - -document.querySelector("#js-seach-form").addEventListener("submit", (event) => { - event.preventDefault(); + loginBtnEl.classList.remove("is-hidden"); - const genre = event.target.querySelector("#genre").value; - // s.searchPlaylists("NO", "market") - s.searchPlaylists(genre) - .then((data) => { - const playlists = data.playlists.items; - console.log(playlists); - // console.log(data); - - playlistContainerEl.innerHTML = ""; - - playlists.forEach((playlist) => { - playlistContainerEl.innerHTML += renderPlaylist({ - title: playlist.name, - }); - }); - }) - .catch((error) => { - console.log(error); - }); -}); - -async function loginSpotifyUser() { - // NOTE: We need to request authorization from the user to access their data. - const authUrl = new URL("https://accounts.spotify.com/authorize"); - - authUrl.searchParams.set("response_type", "token"); - authUrl.searchParams.set("client_id", import.meta.env.VITE_CLIENT_ID); - authUrl.searchParams.set("redirect_uri", import.meta.env.VITE_CALLBACK_URL); - authUrl.searchParams.set("scope", "user-read-private user-read-email"); - // REMOVE THIS IN PRODUCTION - authUrl.searchParams.set("show_dialog", "true"); - - try { - window.location.replace(authUrl); - } catch (error) { - console.log(error); - } + loginBtnEl.addEventListener("click", loginSpotifyUser); } -// TODO: Create a function that renders the playlists -function renderPlaylist({ title = "No title" }) { - return ` -
-

${title}

-
- `; -} +export default s; diff --git a/randomizer.html b/randomizer.html new file mode 100644 index 0000000..fb7be57 --- /dev/null +++ b/randomizer.html @@ -0,0 +1,90 @@ + + + + + + + Bragi Mix | Randomizer + + + + + + +
+ + +
+
+ +
+ + + + + + + + + + + + + + +
+ +
+
+
+
+ + +
+ + + + diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..1ed32d8 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,15 @@ +/* eslint-disable no-undef */ +import { resolve } from "path"; +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + callback: resolve(__dirname, "callback.html"), + randomizer: resolve(__dirname, "randomizer.html"), + }, + }, + }, +});