├── .eslintrc.json ├── favicon.ico ├── public ├── favicon.ico └── assets │ ├── images │ └── playlist_cover.png │ └── icons │ ├── appModeIcon.js │ ├── arrowLeft.js │ ├── arrowRight.js │ ├── searchIcon.js │ ├── HeartIcon.js │ ├── downloadIcon.js │ ├── LibIcon.js │ ├── plusIcon.js │ ├── homeIcon.js │ ├── vicon.svg │ ├── verified.js │ ├── icon.js │ └── logo.js ├── utils ├── numerize.js ├── api.js ├── capitalize.js └── spotifyLogin.js ├── redux ├── actions │ ├── setDeviceId.js │ ├── updateTokenState.js │ ├── clearReducer.js │ ├── setSpotifyPlayer.js │ ├── getUserCountry.js │ ├── api │ │ ├── getMe.js │ │ ├── getPlayerState.js │ │ ├── getSearchResults.js │ │ ├── getCategoryPlaylists.js │ │ ├── getUserTopItems.js │ │ ├── getTopLikedArtists.js │ │ ├── getTopLikedTracks.js │ │ ├── getFeaturedList.js │ │ ├── getNewReleases.js │ │ ├── play_pause_track.js │ │ ├── getRecentlyPlayedLists.js │ │ ├── getBrowseCategories.js │ │ ├── getGenres.js │ │ └── getWorkDetails.js │ └── index.js ├── reducers │ ├── deviceIdReducer.js │ ├── searchGenres.js │ ├── tokenReducer.js │ ├── userReducer.js │ ├── userTopReducer.js │ ├── countryCodeReducer.js │ ├── currentPlayReducer.js │ ├── spotifyPlayer.js │ ├── browseReducer.js │ ├── topTracksReducer.js │ ├── artistsReducer.js │ ├── searchResults.js │ ├── featuredReducer.js │ ├── workViewReducer.js │ ├── newReleaseReducer.js │ ├── recentlyReducer.js │ ├── categoryPageReducer.js │ ├── genrePageReducer.js │ └── index.js └── store │ └── index.js ├── pages ├── collection │ ├── index.jsx │ └── tracks.jsx ├── download │ └── index.jsx ├── api │ └── imageProxy.js ├── search │ ├── category │ │ ├── .module.sass │ │ └── [id].jsx │ ├── .module.sass │ ├── [query].jsx │ └── index.jsx ├── _document.jsx ├── _app.jsx ├── profile │ ├── .module.sass │ └── index.jsx ├── index.jsx ├── [work] │ └── [id] │ │ ├── .module.sass │ │ └── index.jsx └── genre │ └── [genre].jsx ├── next-env.d.ts ├── components ├── NextImage │ └── index.jsx ├── LoaderWrapper │ ├── .module.sass │ └── index.jsx ├── AppSidebar │ ├── .module.sass │ └── index.jsx ├── PlayPauseBtn │ ├── .module.sass │ └── index.jsx ├── NavBtns │ ├── .module.sass │ └── index.jsx ├── PlaylistsRow │ ├── .module.sass │ └── index.jsx ├── ActionsTopBar │ ├── .module.sass │ └── index.jsx ├── SignupBanner │ ├── .module.sass │ └── index.jsx ├── RangeInput │ ├── index.jsx │ └── .module.sass ├── SearchInput │ ├── index.jsx │ └── .module.sass ├── NavLink │ └── index.jsx ├── UserActions │ ├── .module.sass │ └── index.jsx ├── TracksTable │ ├── .module.sass │ └── index.jsx ├── AsideNavList │ ├── .module.sass │ └── index.jsx ├── CategoryComponent │ ├── index.jsx │ └── .module.sass ├── LandingPage │ ├── .module.sass │ └── index.jsx ├── AuthGuard │ └── index.jsx ├── TrackComponent │ ├── .module.sass │ └── index.jsx ├── PlayListComponent │ ├── .module.sass │ └── index.jsx ├── AudioPlayer │ ├── .module.sass │ └── index.jsx └── AppMain │ ├── .module.sass │ └── index.jsx ├── styles ├── variables.sass └── global.sass ├── .gitignore ├── next.config.js ├── README.md └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoycod1ng/spotify-clone/HEAD/favicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoycod1ng/spotify-clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /utils/numerize.js: -------------------------------------------------------------------------------- 1 | export default (value) => { 2 | return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 3 | }; 4 | -------------------------------------------------------------------------------- /utils/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | export default axios.create({ 3 | baseURL: "https://api.spotify.com/v1", 4 | }); 5 | -------------------------------------------------------------------------------- /public/assets/images/playlist_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjoycod1ng/spotify-clone/HEAD/public/assets/images/playlist_cover.png -------------------------------------------------------------------------------- /redux/actions/setDeviceId.js: -------------------------------------------------------------------------------- 1 | export default (payload) => { 2 | return { 3 | type: "SET_DEVICE_ID", 4 | payload, 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /pages/collection/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Collection() { 4 | return
COLLECTION PAGE
; 5 | } 6 | -------------------------------------------------------------------------------- /pages/collection/tracks.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Tracks() { 4 | return
LIKED TRACKS PAGE
; 5 | } 6 | -------------------------------------------------------------------------------- /redux/actions/updateTokenState.js: -------------------------------------------------------------------------------- 1 | export default (payload) => { 2 | return { 3 | type: "UPDATE_TOKEN_STATE", 4 | payload, 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /pages/download/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function DownloadPage() { 4 | return
DOWNLOAD APP PAGE
; 5 | } 6 | -------------------------------------------------------------------------------- /redux/actions/clearReducer.js: -------------------------------------------------------------------------------- 1 | export default (payload) => { 2 | return { 3 | type: "CLEAR_REDUCER", 4 | payload: payload || null, 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/actions/setSpotifyPlayer.js: -------------------------------------------------------------------------------- 1 | export const setSpotifyPlayer = (payload) => { 2 | return { 3 | type: "UPDATE__SPOTIFY_PLAYER", 4 | payload, 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/deviceIdReducer.js: -------------------------------------------------------------------------------- 1 | export default (id = null, action) => { 2 | if (action.type === "SET_DEVICE_ID") { 3 | return action.payload; 4 | } 5 | return id; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/searchGenres.js: -------------------------------------------------------------------------------- 1 | export default (list = [], action) => { 2 | if (action.type === "GET__GENRES") { 3 | return [...list, action.payload]; 4 | } 5 | return list; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/tokenReducer.js: -------------------------------------------------------------------------------- 1 | export default (token = null, action) => { 2 | if (action.type === "UPDATE_TOKEN_STATE") { 3 | return action.payload; 4 | } 5 | return token; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | export default (user = null, action) => { 2 | if (action.type === "UPDATE_USER_STATE") { 3 | return action.payload; 4 | } 5 | return user; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/userTopReducer.js: -------------------------------------------------------------------------------- 1 | export default (state = {}, action) => { 2 | if (action.type === "SET_USER_TOP_ITEMS") { 3 | return action.payload; 4 | } 5 | return state; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/countryCodeReducer.js: -------------------------------------------------------------------------------- 1 | export default (code = null, action) => { 2 | if (action.type === "SET_USER_COUNTRY_CODE") { 3 | return action.payload; 4 | } 5 | return code; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/currentPlayReducer.js: -------------------------------------------------------------------------------- 1 | export default (state = null, action) => { 2 | if (action.type === "GET_CURRENT_PLAY_STATE") { 3 | return action.payload; 4 | } 5 | return state; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/spotifyPlayer.js: -------------------------------------------------------------------------------- 1 | export default (player = null, action) => { 2 | if (action.type === "UPDATE__SPOTIFY_PLAYER") { 3 | return action.payload; 4 | } 5 | return player; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/browseReducer.js: -------------------------------------------------------------------------------- 1 | export default (list = [], action) => { 2 | if (action.type === "GET_BROWSE_CATEGORY_PLAYLISTS") { 3 | return [...list, action.payload]; 4 | } 5 | return list; 6 | }; 7 | -------------------------------------------------------------------------------- /utils/capitalize.js: -------------------------------------------------------------------------------- 1 | export default (string) => { 2 | return string 3 | .split(" ") 4 | .map((el) => { 5 | return el.replace(el.charAt(0), el.charAt(0).toUpperCase()); 6 | }) 7 | .join(" "); 8 | }; 9 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /redux/reducers/topTracksReducer.js: -------------------------------------------------------------------------------- 1 | export default (albums = { msg: null, items: [] }, action) => { 2 | if (action.type === "GET_TOP_LIKED_TRACKS") { 3 | return { msg: action.payload.msg, items: action.payload.items }; 4 | } 5 | return albums; 6 | }; 7 | -------------------------------------------------------------------------------- /components/NextImage/index.jsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export default function NextImage(props) { 4 | // return ; 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /redux/reducers/artistsReducer.js: -------------------------------------------------------------------------------- 1 | export default (artists = { msg: null, items: [] }, action) => { 2 | if (action.type === "GET_TOP_LIKED_ARTISTS") { 3 | return { msg: action.payload.msg, items: action.payload.items }; 4 | } 5 | return artists; 6 | }; 7 | -------------------------------------------------------------------------------- /redux/reducers/searchResults.js: -------------------------------------------------------------------------------- 1 | export default (results = {}, action) => { 2 | if (action.type === "SET_SEARCH_RESULTS") { 3 | return action.payload; 4 | } 5 | if (action.type === "CLEAR_REDUCER") { 6 | return action.payload; 7 | } 8 | return results; 9 | }; 10 | -------------------------------------------------------------------------------- /redux/reducers/featuredReducer.js: -------------------------------------------------------------------------------- 1 | export default (list = { msg: null, items: [] }, action) => { 2 | if (action.type === "GET_FEATURED_PLAYLISTS") { 3 | return { 4 | msg: action.payload.msg, 5 | items: action.payload.items, 6 | }; 7 | } 8 | return list; 9 | }; 10 | -------------------------------------------------------------------------------- /redux/reducers/workViewReducer.js: -------------------------------------------------------------------------------- 1 | export default (view = {}, action) => { 2 | if (action.type === "GET_PLAYLIST|ARTIST|ALBUM__VIEW") { 3 | return action.payload; 4 | } 5 | if (action.type === "CLEAR_REDUCER") { 6 | return action.payload; 7 | } 8 | return view; 9 | }; 10 | -------------------------------------------------------------------------------- /redux/reducers/newReleaseReducer.js: -------------------------------------------------------------------------------- 1 | export default (list = { msg: null, items: [] }, action) => { 2 | if (action.type === "GET_NEW_RELEASES_PLAYLISTS") { 3 | return { 4 | msg: action.payload.msg, 5 | items: [...list.items, action.payload.item], 6 | }; 7 | } 8 | return list; 9 | }; 10 | -------------------------------------------------------------------------------- /redux/reducers/recentlyReducer.js: -------------------------------------------------------------------------------- 1 | export default (list = { msg: null, items: [] }, action) => { 2 | if (action.type === "GET_RECENTLY_PLAYED_PLAYLISTS") { 3 | return { 4 | msg: action.payload.msg, 5 | items: [...list.items, action.payload.item], 6 | }; 7 | } 8 | return list; 9 | }; 10 | -------------------------------------------------------------------------------- /components/LoaderWrapper/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .load_spinner_wrapper 3 | width: 100% 4 | height: 100vh 5 | display: flex 6 | justify-content: center 7 | align-items: center 8 | .grow_spinner 9 | color: $main-color 10 | width: 5rem 11 | height: 5rem -------------------------------------------------------------------------------- /redux/reducers/categoryPageReducer.js: -------------------------------------------------------------------------------- 1 | export default (state = { msg: null, items: [] }, action) => { 2 | if (action.type === "GET_CATEGORY_ALL_PLAYLISTS") { 3 | return { msg: action.payload.msg, items: action.payload.items }; 4 | } 5 | if (action.type === "CLEAR_REDUCER") { 6 | return action.payload; 7 | } 8 | return state; 9 | }; 10 | -------------------------------------------------------------------------------- /components/LoaderWrapper/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | export default function LoaderWrapper() { 4 | return ( 5 |
6 |
7 | Loading... 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /redux/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import reducers from "../reducers"; 4 | 5 | let composeEnhancers = compose; 6 | if (typeof window !== "undefined") { 7 | composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 8 | } 9 | export default createStore(reducers, composeEnhancers(applyMiddleware(thunk))); 10 | -------------------------------------------------------------------------------- /pages/api/imageProxy.js: -------------------------------------------------------------------------------- 1 | // pages/api/imageProxy.ts 2 | 3 | import { withImageProxy } from "@blazity/next-image-proxy"; 4 | 5 | export default withImageProxy({ 6 | whitelistedPatterns: [ 7 | /^http?:\/\/(.*)/, 8 | /^https?:\/\/(.*)/, 9 | /^https?:\/\/(.*).spotifycdn.com/, 10 | /^https?:\/\/(.*).spotifycdn.co/, 11 | /^https?:\/\/(.*).scdn.com/, 12 | /^https?:\/\/(.*).scdn.co/, 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /redux/actions/getUserCountry.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const getUserCountry = () => async (dispatch) => { 4 | try { 5 | const res = await axios.get("https://ipinfo.io?token=1f57143c96e23b"); 6 | dispatch({ 7 | type: "SET_USER_COUNTRY_CODE", 8 | payload: res.country, 9 | }); 10 | } catch (error) { 11 | dispatch({ 12 | type: "SET_USER_COUNTRY_CODE", 13 | payload: "EG", 14 | }); 15 | } 16 | }; 17 | 18 | export default getUserCountry; 19 | -------------------------------------------------------------------------------- /styles/variables.sass: -------------------------------------------------------------------------------- 1 | // Font Settings 2 | $f-size: 14px 3 | $f-weight: 400 4 | $f-family-main: 'poppins' 5 | $f-family-secondary: 'cairo' 6 | 7 | // Colors 8 | // $main-color: #1DB954 9 | 10 | $main-color: #1ED760 11 | $gradient_1: linear-gradient(90deg, #1DB954 0%, #4DD4AC 100%) 12 | $gradient_2: linear-gradient(90deg, #8C68CD 0%, #DE5C9D 100%) 13 | $gradient_3: linear-gradient(90deg, #1DB954 0%, #c6d44d 100%) 14 | 15 | 16 | 17 | // Other 18 | $aside_transition: linear width .2s 19 | 20 | -------------------------------------------------------------------------------- /components/AppSidebar/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .logo_brand_link 3 | display: block 4 | .collapse_aside_btn 5 | all: unset 6 | display: block 7 | .download_app_area 8 | text-align: center 9 | .download_app_btn 10 | color: #fff !important 11 | background: $main-color 12 | font-size: 12px 13 | font-weight: 500 14 | display: inline-flex 15 | padding: 10px 26px 16 | border-radius: 1000px 17 | text-transform: uppercase 18 | letter-spacing: 1px 19 | &:hover 20 | background: $gradient_1 -------------------------------------------------------------------------------- /pages/search/category/.module.sass: -------------------------------------------------------------------------------- 1 | .page_header 2 | background: linear-gradient(var(--text_color) -700% , transparent) 3 | display: flex 4 | align-items: flex-end 5 | h1 6 | padding-top: 10rem 7 | padding-bottom: 3rem 8 | font-size: 4.5rem 9 | word-break: break-all 10 | text-transform: capitalize 11 | font-weight: 900 12 | @media screen and ( max-width: 575px ) 13 | .page_header 14 | h1 15 | font-size: 3.5rem 16 | padding-top: 7rem 17 | padding-bottom: 1rem -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .env 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /redux/actions/api/getMe.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getMe = (token) => async (dispatch) => { 4 | try { 5 | const res = await api.get("/me", { 6 | headers: { 7 | Authorization: `Bearer ${token}`, 8 | }, 9 | }); 10 | dispatch({ 11 | type: "UPDATE_USER_STATE", 12 | payload: res.data, 13 | }); 14 | } catch (error) { 15 | dispatch({ 16 | type: "UPDATE_USER_STATE", 17 | payload: false, 18 | }); 19 | window.localStorage.setItem("token", null); 20 | } 21 | }; 22 | 23 | export default getMe; 24 | -------------------------------------------------------------------------------- /public/assets/icons/appModeIcon.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useTheme } from "next-themes"; 3 | import { IoSunnyOutline, IoMoonOutline } from "react-icons/io5"; 4 | export default function () { 5 | const { theme } = useTheme(""); 6 | const [isDark, setisDark] = useState(null); 7 | useEffect(() => { 8 | if (theme === "dark") setisDark(true); 9 | if (theme === "light") setisDark(false); 10 | }, [theme]); 11 | if (isDark) return ; 12 | if (isDark === false) return ; 13 | if (isDark === null) return <>; 14 | } 15 | -------------------------------------------------------------------------------- /redux/actions/api/getPlayerState.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getPlayerState = (token) => async (dispatch) => { 4 | try { 5 | const res = await api.get("/me/player", { 6 | headers: { 7 | Authorization: `Bearer ${token}`, 8 | }, 9 | }); 10 | console.log(res); 11 | dispatch({ 12 | type: "GET_CURRENT_PLAY_STATE", 13 | payload: res.data, 14 | }); 15 | } catch (error) { 16 | dispatch({ 17 | type: "GET_CURRENT_PLAY_STATE", 18 | payload: false, 19 | }); 20 | } 21 | }; 22 | 23 | export default getPlayerState; 24 | -------------------------------------------------------------------------------- /components/PlayPauseBtn/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .play_pause_track_btn 3 | outline: none 4 | border: 0 5 | width: 100% 6 | height: 100% 7 | border-radius: 50% 8 | background: $main-color 9 | box-shadow: 1px 1px 5px #000000a9 10 | position: relative 11 | svg 12 | position: absolute 13 | left: 50% 14 | top: 50% 15 | transform: translate(calc( -50% + 6%),-50%) 16 | width: 80% 17 | height: 80% 18 | color: var(--bg_color_3) 19 | &.isPlaying 20 | svg 21 | transform: translate(calc( -50% ),-50%) -------------------------------------------------------------------------------- /components/NavBtns/.module.sass: -------------------------------------------------------------------------------- 1 | .app_nav_btns_container 2 | display: flex 3 | align-items: center 4 | justify-content: space-between 5 | .app_nav_btn 6 | all: unset 7 | cursor: pointer 8 | display: flex 9 | justify-content: center 10 | align-items: center 11 | padding: 0.4rem 12 | padding-right: 1.5rem 13 | padding-left: 0 14 | svg 15 | margin-right: .5rem 16 | display: block 17 | width: 24px 18 | height: 24px 19 | &:disabled 20 | opacity: 0.3 21 | cursor: not-allowed -------------------------------------------------------------------------------- /components/PlaylistsRow/.module.sass: -------------------------------------------------------------------------------- 1 | .playlists_row 2 | &:not(:last-of-type) 3 | margin-bottom: 1.5rem 4 | 5 | .see_all_link 6 | color: var(--text-color) 7 | text-transform: uppercase 8 | font-weight: 600 9 | font-size: 12px 10 | .placeholder 11 | background: var(--bg_color_3) 12 | margin-bottom: 0.5rem 13 | height: 16px 14 | width: 180px 15 | display: flex 16 | box-shadow: 1px 1px 2px #00000015 17 | .playlists_row_title 18 | text-transform: capitalize 19 | font-size: 20px 20 | font-weight: 700 21 | padding-right: 1.5rem -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | env:{ 5 | CLIENT_ID:"8f64788e26c9441487c4934944f713d2", 6 | CLIENT_SECRET:"11d1f5c8a5b14cdd95bf86db6d8beb96" 7 | }, 8 | 9 | images: { 10 | domains: [ 11 | "i.scdn.co", 12 | "seed-mix-image.spotifycdn.com", 13 | "charts-images.scdn.co", 14 | "daily-mix.scdn.co", 15 | "mosaic.scdn.co", 16 | "thisis-images.scdn.co", 17 | "seeded-session-images.scdn.co", 18 | "newjams-images.scdn.co", 19 | ], 20 | }, 21 | }; 22 | 23 | module.exports = nextConfig; 24 | -------------------------------------------------------------------------------- /components/ActionsTopBar/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .container 3 | padding-left: 16px 4 | padding-right: 16px 5 | width: 100% 6 | max-width: 1440px 7 | .actions_top_bar 8 | z-index: 7 9 | display: flex 10 | align-items: center 11 | transition: .3s 12 | height: 95px 13 | margin-bottom: -95px 14 | background: transparent 15 | position: sticky 16 | &.stick_top 17 | top: 0 18 | height: 75px 19 | background: var(--bg_color_1) 20 | box-shadow: 1px 1px 5px #00000015 21 | @media screen and ( min-width: 575px ) 22 | .container 23 | padding-left: 32px 24 | padding-right: 32px -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify NextJs Clone App 2 | 3 | 🎧 🎵 **Spotify** clone app using spotify api and spotify SDK with **React.js**, **Next.js** and **Redux** 4 | 5 | live_preview: [https://lotus-spotify-clone.vercel.app](https://lotus-spotify-clone.vercel.app) 6 | 7 | ## Tools & Techniques 🛠👨‍💻: 8 | - ReactJs with NextJs and Redux 9 | - Spotify Api and Spotify SDK 10 | 11 | ## Important Note 📝: 12 | App still in development mode. users are limited. contact me with your spotify email and user to add you 13 | 14 | ## Intialization Steps: 15 | 1. Update API client ID and secret key in `next.config.js` file 16 | 2. Run command ```npm run dev``` 17 | 3. Open ```http://localhost:3000``` on your browser 18 | -------------------------------------------------------------------------------- /components/SignupBanner/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | 3 | .sign_up_banner 4 | color: #FFF 5 | background: $gradient_2 6 | height: 96px 7 | display: flex 8 | justify-content: center 9 | align-items: center 10 | padding: 0 18px 11 | .overview_spotify 12 | text-transform: uppercase 13 | font-weight: 600 14 | .banner_content 15 | display: block 16 | font-size: 14px 17 | .banner_signup_cta 18 | background: #fff 19 | color: #3D8BFD 20 | font-size: 13px 21 | font-weight: 600 22 | letter-spacing: 2px 23 | text-transform: uppercase 24 | padding: 0.65rem 3rem 25 | border-radius: 1000px -------------------------------------------------------------------------------- /redux/actions/api/getSearchResults.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getSearchResults = (token, q, cc) => async (dispatch) => { 4 | try { 5 | const res = await api.get("/search", { 6 | headers: { 7 | Authorization: `Bearer ${ 8 | token || window.localStorage.getItem("token") 9 | }`, 10 | }, 11 | params: { 12 | q, 13 | type: "track,artist,album,playlist", 14 | market: cc, 15 | limit: 5, 16 | }, 17 | }); 18 | dispatch({ type: "SET_SEARCH_RESULTS", payload: res.data }); 19 | } catch (error) { 20 | dispatch({ type: "SET_SEARCH_RESULTS", payload: null }); 21 | } 22 | }; 23 | 24 | export default getSearchResults; 25 | -------------------------------------------------------------------------------- /components/NavBtns/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | import { FiChevronLeft } from "react-icons/fi"; 4 | import { useRouter } from "next/router"; 5 | 6 | export default function NavBtns() { 7 | const router = useRouter(); 8 | 9 | return ( 10 |
11 |
12 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /public/assets/icons/arrowLeft.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return ( 5 | 12 | 20 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /public/assets/icons/arrowRight.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return ( 5 | 12 | 20 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/RangeInput/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import InputRange from "react-input-range"; 3 | import "react-input-range/lib/css/index.css"; 4 | import styles from "./.module.sass"; 5 | 6 | export default function RangeInput({ 7 | onChange, 8 | maxValue, 9 | value, 10 | onCompleteChange, 11 | step, 12 | }) { 13 | return ( 14 |
15 | { 21 | onChange(value); 22 | }} 23 | formatLabel={() => {}} 24 | onChangeComplete={(value) => { 25 | onCompleteChange(value); 26 | }} 27 | /> 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/RangeInput/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .range_input_wrapper 3 | > div > div 4 | background: #80808041 5 | > div 6 | background: var(--bs-gray-600) 7 | background: $gradient_3 8 | > span > div 9 | border: 0 10 | background: var(--text_color) 11 | transform: scale(.75) !important 12 | &:hover 13 | > div > div 14 | > div 15 | background: $gradient_3 16 | > span > div 17 | visibility: visible 18 | @media screen and ( min-width: 991px ) 19 | .range_input_wrapper 20 | > div > div 21 | > div 22 | background: var(--text_color) 23 | > span > div 24 | visibility: hidden 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /components/SignupBanner/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | 4 | export default function SignupBanner() { 5 | return ( 6 |
7 |
8 | Important Note 9 |

10 | Spotify SDK that used in this app to play music tracks is only 11 | available for premium accounts. 12 |

13 |
14 | 20 | subscribe now 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/SearchInput/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | import SearchIcon from "../../public/assets/icons/searchIcon"; 4 | import { useRouter } from "next/router"; 5 | import debounce from "debounce"; 6 | 7 | export default function SearchInput() { 8 | const router = useRouter(); 9 | if (router.pathname.startsWith("/search")) 10 | return ( 11 |
12 | 13 |
e.preventDefault()}> 14 | { 18 | router.push(`/search/${e.target.value}`); 19 | }, 1000)} 20 | /> 21 |
22 |
23 | ); 24 | return <>; 25 | } 26 | -------------------------------------------------------------------------------- /utils/spotifyLogin.js: -------------------------------------------------------------------------------- 1 | export const CLIENT_ID = process.env.CLIENT_ID; 2 | export const CLIENT_SECRET = process.env.CLIENT_SECRET; 3 | export const REDIRECT_URI = 4 | typeof window !== "undefined" 5 | ? window.location.origin 6 | : "https://lotus-spotify-clone.vercel.app"; 7 | export const AUTH_ENDPOINT = "https://accounts.spotify.com/authorize"; 8 | export const TOKEN_ENDPOINT = `https://accounts.spotify.com/api/token`; 9 | export const RESPONSE_TYPE = "code"; 10 | export const SCOPES = 11 | "user-read-recently-played,playlist-modify-private,user-modify-playback-state,user-read-playback-state,user-read-currently-playing,user-top-read,user-read-currently-playing,streaming,user-read-email,user-read-private"; 12 | export const SPOTIFY_LOGIN = `${AUTH_ENDPOINT}?client_id=${CLIENT_ID}&scope=${SCOPES}&redirect_uri=${REDIRECT_URI}&response_type=${RESPONSE_TYPE}`; 13 | -------------------------------------------------------------------------------- /components/NavLink/index.jsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import Link from "next/link"; 3 | import PropTypes from "prop-types"; 4 | 5 | NavLink.propTypes = { 6 | href: PropTypes.string.isRequired, 7 | exact: PropTypes.bool, 8 | }; 9 | 10 | NavLink.defaultProps = { 11 | exact: false, 12 | }; 13 | 14 | function NavLink({ href, exact, children, styles, ...props }) { 15 | const { pathname } = useRouter(); 16 | const isActive = exact ? pathname === href : pathname.startsWith(href); 17 | 18 | if (isActive) { 19 | props.className += ` ${styles.active}`; 20 | } 21 | 22 | return ( 23 |
  • 24 |
    25 | 26 | {children} 27 | 28 |
    29 |
  • 30 | ); 31 | } 32 | export default NavLink; 33 | -------------------------------------------------------------------------------- /pages/_document.jsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default class _document extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 |
    17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /public/assets/icons/searchIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return ( 5 | 12 | 20 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/SearchInput/.module.sass: -------------------------------------------------------------------------------- 1 | .search_form_container 2 | display: flex 3 | align-items: center 4 | padding: .4rem 0.5rem 5 | border-radius: 100px 6 | background: var(--text_color) 7 | background: var(--bg_color) 8 | svg 9 | width: 22px 10 | height: 22px 11 | margin-right: 0.75rem 12 | path 13 | stroke: var(--bg_color) 14 | stroke: var(--text_color) 15 | input 16 | border: 0 17 | outline: none 18 | max-width: 160px 19 | padding-right: 0.5rem 20 | background: transparent 21 | color: var(--bg_color) 22 | color: var(--text_color) 23 | &::placeholder 24 | font-size: 11px 25 | color: var(--bg_color) 26 | color: var(--text_color) 27 | @media screen and (min-width: 390px) 28 | .search_form_container 29 | border: 1px solid var(--bs-gray-400) 30 | -------------------------------------------------------------------------------- /pages/search/.module.sass: -------------------------------------------------------------------------------- 1 | ._row_title 2 | text-transform: capitalize 3 | font-size: 20px 4 | font-weight: 700 5 | .navigation_btns_container 6 | z-index: 2 7 | height: 100% 8 | position: relative 9 | .swiper_button_prev, 10 | .swiper_button_next 11 | display: grid 12 | place-items: center 13 | color: var(--bg_color) 14 | background: var(--text_color ) 15 | font-size: 1.25rem 16 | box-shadow: 1px 1px 10px #00000030 17 | width: 45px 18 | height: 45px 19 | left: 0 20 | top: 120px 21 | transform: translateX(-50%) 22 | border-radius: 50% 23 | padding-right: 3px 24 | &::after,&::before 25 | display: none 26 | .swiper_button_next 27 | padding-right: 0 28 | padding-left: 3px 29 | left: auto 30 | right: 0 31 | transform: translateX(50%) 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/assets/icons/HeartIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return ( 5 | 12 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /redux/actions/api/getCategoryPlaylists.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getCategoryPlaylists = 4 | (id, token, country, limit) => async (dispatch) => { 5 | try { 6 | const res = await api.get(`/browse/categories/${id}/playlists`, { 7 | headers: { 8 | Authorization: `Bearer ${ 9 | token || window.localStorage.getItem("token") 10 | }`, 11 | }, 12 | params: { 13 | country: country || "EG", 14 | limit: limit || 5, 15 | }, 16 | }); 17 | dispatch({ 18 | type: "GET_CATEGORY_ALL_PLAYLISTS", 19 | payload: { 20 | msg: id, 21 | items: res.data.playlists.items, 22 | }, 23 | }); 24 | } catch (error) { 25 | dispatch({ 26 | type: "GET_CATEGORY_ALL_PLAYLISTS", 27 | payload: { msg: null, items: null }, 28 | }); 29 | } 30 | }; 31 | 32 | export default getCategoryPlaylists; 33 | -------------------------------------------------------------------------------- /public/assets/icons/downloadIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return ( 5 | 12 | 18 | 24 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /redux/actions/api/getUserTopItems.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getUserTopItems = (token, limit) => async (dispatch) => { 4 | try { 5 | const artists = await api.get("/me/top/artists", { 6 | headers: { 7 | Authorization: `Bearer ${token}`, 8 | }, 9 | params: { 10 | time_range: "short_term", 11 | limit: limit || 5, 12 | }, 13 | }); 14 | const tracks = await api.get("/me/top/tracks", { 15 | headers: { 16 | Authorization: `Bearer ${token}`, 17 | }, 18 | params: { 19 | time_range: "short_term", 20 | limit: limit || 5, 21 | }, 22 | }); 23 | dispatch({ 24 | type: "SET_USER_TOP_ITEMS", 25 | payload: { tracks: tracks.data.items, artists: artists.data.items }, 26 | }); 27 | } catch (error) { 28 | dispatch({ 29 | type: "SET_USER_TOP_ITEMS", 30 | payload: null, 31 | }); 32 | } 33 | }; 34 | 35 | export default getUserTopItems; 36 | -------------------------------------------------------------------------------- /public/assets/icons/LibIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return ( 5 | 12 | 20 | 28 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /public/assets/icons/plusIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return ( 5 | 12 | 18 | 26 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /redux/actions/api/getTopLikedArtists.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getTopLikedArtists = (token, limit) => async (dispatch) => { 4 | try { 5 | const res = await api.get("/me/top/artists", { 6 | headers: { 7 | Authorization: `Bearer ${ 8 | token || window.localStorage.getItem("token") 9 | }`, 10 | }, 11 | params: { 12 | limit: limit || 5, 13 | }, 14 | }); 15 | if (res.data.items.length <= 5) 16 | dispatch({ 17 | type: "GET_TOP_LIKED_ARTISTS", 18 | payload: { msg: "artists you like", items: res.data.items }, 19 | }); 20 | else { 21 | dispatch({ 22 | type: "GET_TOP_LIKED_ARTISTS_ALL", 23 | payload: { msg: "artists you like", items: res.data.items }, 24 | }); 25 | } 26 | } catch (error) { 27 | dispatch({ 28 | type: "GET_TOP_LIKED_ARTISTS", 29 | payload: { msg: "Something wrong happened!", items: [] }, 30 | }); 31 | } 32 | }; 33 | 34 | export default getTopLikedArtists; 35 | -------------------------------------------------------------------------------- /redux/actions/api/getTopLikedTracks.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getTopLikedTracks = (token, limit) => async (dispatch) => { 4 | try { 5 | const res = await api.get("/me/top/tracks", { 6 | headers: { 7 | Authorization: `Bearer ${ 8 | token || window.localStorage.getItem("token") 9 | }`, 10 | }, 11 | params: { 12 | limit: limit || 5, 13 | }, 14 | }); 15 | const items = res.data.items.map((item) => item.album); 16 | if (items.length <= 5) 17 | dispatch({ 18 | type: "GET_TOP_LIKED_TRACKS", 19 | payload: { msg: "more tracks you like", items }, 20 | }); 21 | else { 22 | dispatch({ 23 | type: "GET_TOP_LIKED_TRACKS_ALL", 24 | payload: { msg: "more tracks you like", items }, 25 | }); 26 | } 27 | } catch (error) { 28 | dispatch({ 29 | type: "GET_TOP_LIKED_TRACKS", 30 | payload: { msg: "Something wrong happened!", items: [] }, 31 | }); 32 | } 33 | }; 34 | 35 | export default getTopLikedTracks; 36 | -------------------------------------------------------------------------------- /pages/_app.jsx: -------------------------------------------------------------------------------- 1 | // Global Styling And Fonts Import 2 | import "bootstrap/dist/css/bootstrap.min.css"; 3 | import "../styles/fonts.sass"; 4 | import "../styles/global.sass"; 5 | 6 | // Set REDUX 7 | import store from "../redux/store"; 8 | import { Provider } from "react-redux"; 9 | 10 | // Theme Settings 11 | import { ThemeProvider } from "next-themes"; 12 | 13 | // Components 14 | import AuthGuard from "../components/AuthGuard"; 15 | import AppMain from "../components/AppMain"; 16 | import Head from "next/head"; 17 | 18 | export default function _app({ Component, pageProps }) { 19 | return ( 20 | // <>APP 21 | 22 | 23 | 24 | Spotify App | Enjoy Music 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components/UserActions/.module.sass: -------------------------------------------------------------------------------- 1 | .user_actions_area 2 | background: var(--bg_color_1) 3 | display: inline-flex 4 | align-items: center 5 | padding: .3rem 6 | border-radius: 1000px 7 | .logout_btn 8 | padding: 0 .5rem 9 | a 10 | color: var(--text-color) 11 | svg 12 | width: 20px 13 | height: 20px 14 | color: var(--bs-gray-500) 15 | .user_profile_btn 16 | text-align: left !important 17 | display: flex 18 | align-items: center 19 | color: var(--text-color) 20 | span 21 | line-height: 15px 22 | margin-left: 0.5rem 23 | font-size: 11px 24 | max-width: 90px 25 | .user_avatar 26 | display: grid 27 | place-items: center 28 | border-radius: 50% 29 | width: 36px 30 | height: 36px 31 | border: 2px solid var(--bs-gray-500) 32 | img 33 | object-fit: cover 34 | border-radius: 50% -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-clone-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@blazity/next-image-proxy": "^1.0.2", 13 | "axios": "^0.26.1", 14 | "bootstrap": "^5.1.3", 15 | "debounce": "^1.2.1", 16 | "dotenv": "^16.0.0", 17 | "fast-average-color": "^7.1.0", 18 | "moment": "^2.29.1", 19 | "next": "12.1.0", 20 | "next-themes": "^0.1.1", 21 | "node-sass": "^7.0.1", 22 | "randomcolor": "^0.6.2", 23 | "react": "17.0.2", 24 | "react-dom": "17.0.2", 25 | "react-icons": "^4.3.1", 26 | "react-input-range": "^1.3.0", 27 | "react-redux": "^7.2.6", 28 | "redux": "^4.1.2", 29 | "redux-thunk": "^2.4.1", 30 | "swiper": "^8.0.7" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^17.0.21", 34 | "eslint": "8.11.0", 35 | "eslint-config-next": "12.1.0", 36 | "find-cache-dir": "^3.3.2", 37 | "typescript": "^4.9.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /redux/reducers/genrePageReducer.js: -------------------------------------------------------------------------------- 1 | export default (list = { msg: null, items: [] }, action) => { 2 | if (action.type === "GET_GENRE_RECENT_PLAYLISTS_ALL") { 3 | return { 4 | msg: action.payload.msg, 5 | items: [...list.items, action.payload.item], 6 | }; 7 | } 8 | if (action.type === "GET_NEW_RELEASES_PLAYLISTS_ALL") { 9 | return { 10 | msg: action.payload.msg, 11 | items: [...list.items, action.payload.item], 12 | }; 13 | } 14 | if (action.type === "GET_GENRE_FEATURED_PLAYLISTS") { 15 | return { 16 | msg: action.payload.msg, 17 | items: action.payload.items, 18 | }; 19 | } 20 | 21 | if (action.type === "GET_TOP_LIKED_ARTISTS_ALL") { 22 | return { 23 | msg: action.payload.msg, 24 | items: action.payload.items, 25 | }; 26 | } 27 | if (action.type === "GET_TOP_LIKED_TRACKS_ALL") { 28 | return { 29 | msg: action.payload.msg, 30 | items: action.payload.items, 31 | }; 32 | } 33 | 34 | if (action.type === "CLEAR_REDUCER") { 35 | return { msg: null, items: [] }; 36 | } 37 | return list; 38 | }; 39 | -------------------------------------------------------------------------------- /components/ActionsTopBar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | import NavBtns from "../NavBtns"; 4 | import SearchInput from "../SearchInput"; 5 | import UserActions from "../UserActions"; 6 | import { connect } from "react-redux"; 7 | 8 | export default connect((state) => state)(function ActionsTopBar({ 9 | isSticky, 10 | workView, 11 | }) { 12 | return ( 13 |
    19 |
    20 |
    21 |
    22 |
    23 | 24 | 25 |
    26 |
    27 |
    28 | 29 |
    30 |
    31 |
    32 |
    33 | ); 34 | }); 35 | -------------------------------------------------------------------------------- /redux/actions/api/getFeaturedList.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getFeaturedList = (token, country, limit) => async (dispatch) => { 4 | try { 5 | const res = await api.get("/browse/featured-playlists", { 6 | headers: { 7 | Authorization: `Bearer ${ 8 | token || window?.localStorage?.getItem("token") 9 | }`, 10 | }, 11 | params: { 12 | country: country || "EG", 13 | limit: limit || 5, 14 | }, 15 | }); 16 | if (res.data.playlists.items.length <= 5) 17 | dispatch({ 18 | type: "GET_FEATURED_PLAYLISTS", 19 | payload: { msg: res.data.message, items: res.data.playlists.items }, 20 | }); 21 | else { 22 | dispatch({ 23 | type: "GET_GENRE_FEATURED_PLAYLISTS", 24 | payload: { msg: res.data.message, items: res.data.playlists.items }, 25 | }); 26 | } 27 | } catch (error) { 28 | dispatch({ 29 | type: "GET_FEATURED_PLAYLISTS", 30 | payload: { msg: "Something wrong happened!", items: [] }, 31 | }); 32 | } 33 | }; 34 | 35 | export default getFeaturedList; 36 | -------------------------------------------------------------------------------- /styles/global.sass: -------------------------------------------------------------------------------- 1 | @import './variables' 2 | 3 | *, 4 | *::after, 5 | *::before 6 | padding: 0 7 | margin: 0 8 | 9 | // LIGHT AND DARK MODE SETTINGS 10 | html[data-theme='dark'] 11 | --text_color: #fff 12 | --bg_color: #0a0a0a 13 | --bg_color_1: #0b0b0b 14 | --bg_color_2: #0d0d0d 15 | --bg_color_3: #151515 16 | --bg_color_4: #202020 17 | html[data-theme='light'] 18 | --text_color: #121212 19 | --bg_color: #fff 20 | --bg_color_1: #fefefe 21 | --bg_color_2: #f2f2f2 22 | --bg_color_3: #fefefe 23 | --bg_color_4: #fff 24 | 25 | 26 | body 27 | font-size: $f-size 28 | font-family: $f-family-main,$f-family-secondary 29 | font-weight: $f-weight 30 | color: var(--text_color) 31 | background: var(--bg_color) 32 | overflow: hidden 33 | a 34 | text-decoration: none 35 | .app_svg_path 36 | fill: var(--text_color) 37 | .app_svg_path_stroke 38 | stroke: var(--text_color) 39 | .container 40 | padding-left: 18px 41 | padding-right: 18px 42 | margin: 0 auto 43 | max-width: 1440px 44 | .swiper-button-disabled 45 | display: none !important -------------------------------------------------------------------------------- /components/TracksTable/.module.sass: -------------------------------------------------------------------------------- 1 | .tracks_table 2 | .tracks_table_head 3 | display: flex 4 | z-index: 5 5 | font-size: 13px 6 | text-transform: uppercase 7 | position: sticky 8 | top: 74px 9 | color: var(--bs-gray-600) 10 | svg 11 | width: 22px 12 | height: 22px 13 | .head_details_row 14 | padding: .6rem 0 15 | border-bottom: 1px solid var(--bs-gray-600) 16 | align-items: center 17 | &.top_fixed 18 | border: 0 19 | color: var(--text_color) 20 | background: var(--bg_color_4) 21 | box-shadow: 1px 1px 5px #00000020 22 | .head_details_row 23 | border-color: var(--bg_color_4) 24 | ._playlist_placeholder, 25 | .icon_playlist_placeholder 26 | box-shadow: 1px 1px 5px #00000020 27 | display: inline-flex 28 | background: var(--bg_color_4) 29 | height: 14px 30 | width: 100px 31 | .icon_playlist_placeholder 32 | display: inline-flex 33 | width: 14px 34 | 35 | 36 | -------------------------------------------------------------------------------- /components/PlaylistsRow/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | import Link from "next/link"; 4 | import PlayListComponent from "../PlayListComponent"; 5 | 6 | export default function PlaylistsRow({ content, link, placeholder }) { 7 | return ( 8 |
    9 |
    10 | {content && content.msg ? ( 11 |

    {content.msg}

    12 | ) : ( 13 | 14 | )} 15 | {content?.items?.length && link ? ( 16 | 17 | 18 | see all 19 | 20 | 21 | ) : null} 22 |
    23 |
    24 | {content?.items?.length 25 | ? content?.items?.map((item, i) => ( 26 | 27 | )) 28 | : [...Array(placeholder)]?.map((el, i) => ( 29 | 30 | ))} 31 |
    32 |
    33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import user from "./userReducer"; 3 | import token from "./tokenReducer"; 4 | import featuredPlaylists from "./featuredReducer"; 5 | import recentlyPlaylists from "./recentlyReducer"; 6 | import artistsPlaylists from "./artistsReducer"; 7 | import albumsPlaylists from "./topTracksReducer"; 8 | import newReleasePlaylists from "./newReleaseReducer"; 9 | import genrePlaylists from "./genrePageReducer"; 10 | import countryCode from "./countryCodeReducer"; 11 | import browseCategories from "./browseReducer"; 12 | import allCategories from "./searchGenres"; 13 | import workView from "./workViewReducer"; 14 | import spotifyPlayer from "./spotifyPlayer"; 15 | import deviceID from "./deviceIdReducer"; 16 | import categoryPage from "./categoryPageReducer"; 17 | import searchResults from "./searchResults"; 18 | import userTopItems from "./userTopReducer"; 19 | export default combineReducers({ 20 | user, 21 | token, 22 | featuredPlaylists, 23 | recentlyPlaylists, 24 | artistsPlaylists, 25 | albumsPlaylists, 26 | newReleasePlaylists, 27 | genrePlaylists, 28 | countryCode, 29 | browseCategories, 30 | allCategories, 31 | workView, 32 | spotifyPlayer, 33 | deviceID, 34 | categoryPage, 35 | searchResults, 36 | userTopItems, 37 | }); 38 | -------------------------------------------------------------------------------- /components/UserActions/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | import { AiOutlinePoweroff } from "react-icons/ai"; 4 | import Link from "next/link"; 5 | import Image from "next/image"; 6 | import { connect } from "react-redux"; 7 | export default connect((state) => state)(function ({ user }) { 8 | return ( 9 |
    10 | 11 | 12 |
    13 | user_avatar 20 |
    21 | {user?.display_name} 22 |
    23 | 24 | 37 |
    38 | ); 39 | }); 40 | -------------------------------------------------------------------------------- /components/PlayPauseBtn/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | import { BiPlay, BiPause } from "react-icons/bi"; 4 | import { playPauseTrack } from "../../redux/actions"; 5 | import { connect } from "react-redux"; 6 | 7 | const mapStateToProps = (state) => state; 8 | const mapDispatchToProps = { playPauseTrack }; 9 | 10 | function PlayPauseBtn({ 11 | isPlaying, 12 | uri, 13 | size, 14 | playPauseTrack, 15 | token, 16 | deviceID, 17 | spotifyPlayer, 18 | offset, 19 | }) { 20 | if (token && deviceID && uri) 21 | return ( 22 | 39 | ); 40 | return <>; 41 | } 42 | export default connect(mapStateToProps, mapDispatchToProps)(PlayPauseBtn); 43 | -------------------------------------------------------------------------------- /redux/actions/api/getNewReleases.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getNewReleases = (token, country, limit) => async (dispatch) => { 4 | try { 5 | const res_1 = await api.get("/browse/new-releases", { 6 | headers: { 7 | Authorization: `Bearer ${ 8 | token || window.localStorage.getItem("token") 9 | }`, 10 | }, 11 | params: { 12 | country: country || "EG", 13 | limit: limit || 5, 14 | }, 15 | }); 16 | const ids = [...new Set(res_1.data.albums.items.map((item) => item.id))]; 17 | ids.map(async (id) => { 18 | const res = await api.get(`/albums/${id}`, { 19 | headers: { 20 | Authorization: `Bearer ${ 21 | token || window.localStorage.getItem("token") 22 | }`, 23 | }, 24 | }); 25 | if (res_1.data.albums.items.length <= 5) 26 | dispatch({ 27 | type: "GET_NEW_RELEASES_PLAYLISTS", 28 | payload: { msg: "new released", item: res.data }, 29 | }); 30 | else { 31 | dispatch({ 32 | type: "GET_NEW_RELEASES_PLAYLISTS_ALL", 33 | payload: { msg: "new released", item: res.data }, 34 | }); 35 | } 36 | }); 37 | } catch (error) { 38 | dispatch({ 39 | type: "GET_NEW_RELEASES_PLAYLISTS", 40 | payload: { msg: "Something wrong happened!", items: [] }, 41 | }); 42 | } 43 | }; 44 | 45 | export default getNewReleases; 46 | -------------------------------------------------------------------------------- /components/AsideNavList/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .app_navigation 3 | .app_navigation_list 4 | list-style: none 5 | padding: 0 6 | margin: 0 7 | &::-webkit-scrollbar 8 | display: none 9 | .app_nav_item 10 | position: relative 11 | margin: .2rem 0 12 | .app_nav_link 13 | all: unset 14 | cursor: pointer 15 | display: flex 16 | align-items: center 17 | font-size: 12px 18 | text-transform: capitalize 19 | padding: 8px 16px 20 | border-radius: 1000px 21 | span 22 | margin-left: .5rem 23 | svg 24 | width: 22px 25 | height: 22px 26 | &.active 27 | color: #fff 28 | svg path 29 | stroke: #fff 30 | &::after 31 | content: '' 32 | position: absolute 33 | top: 0 34 | bottom: 0 35 | left: 0 36 | height: 100% 37 | width: 4px 38 | background: $main-color 39 | .app_nav_link 40 | background: $gradient_1 41 | .list_divider 42 | margin: 0.6rem 0 43 | background: $gradient_1 -------------------------------------------------------------------------------- /redux/actions/api/play_pause_track.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const playPauseTrack = 4 | (deviceID, token, spotify_uri, offset) => async (dispatch) => { 5 | await api.put( 6 | `/me/player`, 7 | { device_ids: [deviceID], play: false }, 8 | { 9 | headers: { 10 | "Content-Type": "application/json", 11 | Authorization: `Bearer ${token}`, 12 | }, 13 | } 14 | ); 15 | if (spotify_uri) 16 | if (offset) { 17 | await api.put( 18 | `/me/player/play?device_id=${deviceID}`, 19 | { 20 | context_uri: spotify_uri, 21 | offset: { 22 | position: offset, 23 | }, 24 | position_ms: 0, 25 | }, 26 | { 27 | headers: { 28 | "Content-Type": "application/json", 29 | Authorization: `Bearer ${token}`, 30 | }, 31 | } 32 | ); 33 | } else { 34 | await api.put( 35 | `/me/player/play?device_id=${deviceID}`, 36 | { 37 | context_uri: spotify_uri, 38 | position_ms: 0, 39 | }, 40 | { 41 | headers: { 42 | "Content-Type": "application/json", 43 | Authorization: `Bearer ${token}`, 44 | }, 45 | } 46 | ); 47 | } 48 | dispatch({ type: "", action: {} }); 49 | }; 50 | 51 | export default playPauseTrack; 52 | -------------------------------------------------------------------------------- /components/CategoryComponent/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | import NextImage from "../NextImage"; 4 | import Link from "next/link"; 5 | export default function StyledCategoryComponent({ size, category }) { 6 | return ( 7 |
    14 | 15 | 16 |
    22 | {category ? ( 23 | {category.name} 24 | ) : ( 25 | 26 | )} 27 |
    28 | {category ? ( 29 | 37 | ) : null} 38 |
    39 |
    40 |
    41 | 42 |
    43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /redux/actions/index.js: -------------------------------------------------------------------------------- 1 | import updateTokenState from "./updateTokenState"; 2 | import getFeaturedList from "./api/getFeaturedList"; 3 | import getMe from "./api/getMe"; 4 | import getRecentlyPlayedLists from "./api/getRecentlyPlayedLists"; 5 | import getTopLikedArtists from "./api/getTopLikedArtists"; 6 | import getTopLikedTracks from "./api/getTopLikedTracks"; 7 | import getNewReleases from "./api/getNewReleases"; 8 | import clearReducer from "./clearReducer"; 9 | import getUserCountry from "./getUserCountry"; 10 | import getBrowseCategories from "./api/getBrowseCategories"; 11 | import getCategoryPlaylists from "./api/getCategoryPlaylists"; 12 | import getGenres from "./api/getGenres"; 13 | import getWorkDetails from "./api/getWorkDetails"; 14 | import { setSpotifyPlayer } from "./setSpotifyPlayer"; 15 | import getPlayerState from "./api/getPlayerState"; 16 | import setDeviceId from "./setDeviceId"; 17 | import playPauseTrack from "./api/play_pause_track"; 18 | import getSearchResults from "./api/getSearchResults"; 19 | import getUserTopItems from "./api/getUserTopItems"; 20 | export { 21 | getMe, 22 | getFeaturedList, 23 | updateTokenState, 24 | getTopLikedTracks, 25 | getTopLikedArtists, 26 | getRecentlyPlayedLists, 27 | getNewReleases, 28 | clearReducer, 29 | getUserCountry, 30 | getBrowseCategories, 31 | getCategoryPlaylists, 32 | getGenres, 33 | getWorkDetails, 34 | setSpotifyPlayer, 35 | getPlayerState, 36 | setDeviceId, 37 | getSearchResults, 38 | playPauseTrack, 39 | getUserTopItems, 40 | }; 41 | -------------------------------------------------------------------------------- /redux/actions/api/getRecentlyPlayedLists.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getRecentlyPlayedLists = (token, limit) => async (dispatch) => { 4 | try { 5 | const recentTracks = await api.get("/me/player/recently-played", { 6 | headers: { 7 | Authorization: `Bearer ${ 8 | token || window.localStorage.getItem("token") 9 | }`, 10 | }, 11 | params: { 12 | limit: limit || 5, 13 | }, 14 | }); 15 | const ids = [ 16 | ...new Set( 17 | recentTracks.data.items.map((item) => item.context.uri.split(":")[2]) 18 | ), 19 | ]; 20 | 21 | ids.map(async (id) => { 22 | const res = await api.get(`/playlists/${id}`, { 23 | headers: { 24 | Authorization: `Bearer ${ 25 | token || window.localStorage.getItem("token") 26 | }`, 27 | }, 28 | }); 29 | if (recentTracks.data.items.length <= 5) 30 | dispatch({ 31 | type: "GET_RECENTLY_PLAYED_PLAYLISTS", 32 | payload: { msg: "recently played", item: res.data }, 33 | }); 34 | else { 35 | dispatch({ 36 | type: "GET_GENRE_RECENT_PLAYLISTS_ALL", 37 | payload: { msg: "recently played", item: res.data }, 38 | }); 39 | } 40 | }); 41 | } catch (error) { 42 | dispatch({ 43 | type: "GET_RECENTLY_PLAYED_PLAYLISTS", 44 | payload: { msg: "Something wrong happened!", items: [] }, 45 | }); 46 | } 47 | }; 48 | 49 | export default getRecentlyPlayedLists; 50 | -------------------------------------------------------------------------------- /components/CategoryComponent/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .category_component_styled 3 | height: 195px 4 | overflow: hidden 5 | position: relative 6 | margin: 1.25rem 0 7 | padding: 24px 20px 8 | color: #fff 9 | background: var(--bg_color_1) 10 | text-transform: capitalize 11 | border-radius: 8px 12 | box-shadow: 1px 1px 10px #00000015 13 | .category_name 14 | font-size: 1.25rem 15 | width: 70% 16 | line-height: 0 17 | font-weight: 700 18 | text-shadow: 1px 1px 2px #00000080 19 | // color: var(--text_color) 20 | .category_name_placeholder 21 | background: var(--bg_color_2) 22 | box-shadow: 11px 1px 10px #00000018 23 | display: block 24 | height: 1rem 25 | width: 130px 26 | .category_cover_img_wrapper 27 | background: var(--bg_color_2) 28 | position: absolute 29 | right: 0 30 | bottom: 0 31 | width: 110px 32 | height: 110px 33 | transform: rotate(30deg) translate(20px,0px) 34 | box-shadow: -1px -1px 10px #00000035 35 | &.size_large 36 | height: 220px 37 | .category_cover_img_wrapper 38 | width: 140px 39 | height: 140px 40 | transform: rotate(30deg) translate(25px,0px) 41 | .category_name_placeholder 42 | width: 220px 43 | .category_name 44 | font-size: 2.25rem 45 | line-height: 2rem 46 | @media screen and ( min-width:1366px ) 47 | .col_xxl_2_5 48 | width: 20% -------------------------------------------------------------------------------- /public/assets/icons/homeIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return ( 5 | 12 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /redux/actions/api/getBrowseCategories.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | 3 | const getBrowseCategories = 4 | (token, country, cLimit, offset) => async (dispatch) => { 5 | try { 6 | const categories = await api.get("/browse/categories", { 7 | headers: { 8 | Authorization: `Bearer ${ 9 | token || window.localStorage.getItem("token") 10 | }`, 11 | }, 12 | params: { 13 | country: country || "EG", 14 | limit: cLimit || 5, 15 | offset: offset || 0, 16 | }, 17 | }); 18 | const ids = categories.data.categories.items.map((item) => item.id); 19 | ids.map(async (id, i) => { 20 | try { 21 | const res = await api.get(`/browse/categories/${id}/playlists`, { 22 | headers: { 23 | Authorization: `Bearer ${ 24 | token || window.localStorage.getItem("token") 25 | }`, 26 | }, 27 | params: { 28 | country: country || "EG", 29 | limit: 5, 30 | }, 31 | }); 32 | if (res.data.playlists.items.length) 33 | dispatch({ 34 | type: "GET_BROWSE_CATEGORY_PLAYLISTS", 35 | payload: { 36 | id: ids[i], 37 | msg: categories.data.categories.items[i].name, 38 | items: res.data.playlists.items, 39 | }, 40 | }); 41 | } catch (error) { 42 | dispatch({ 43 | type: "GET_BROWSE_CATEGORY_PLAYLISTS", 44 | payload: { msg: null, items: null }, 45 | }); 46 | } 47 | }); 48 | } catch (error) { 49 | dispatch({ 50 | type: "GET_BROWSE_CATEGORY_PLAYLISTS", 51 | payload: { msg: null, items: null }, 52 | }); 53 | } 54 | }; 55 | 56 | export default getBrowseCategories; 57 | -------------------------------------------------------------------------------- /pages/search/category/[id].jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Head from "next/head"; 3 | import { useRouter } from "next/router"; 4 | import { connect } from "react-redux"; 5 | import { getCategoryPlaylists, clearReducer } from "../../../redux/actions"; 6 | import { container } from "../../../components/AppMain/.module.sass"; 7 | import PlaylistsRow from "../../../components/PlaylistsRow"; 8 | import styles from "./.module.sass"; 9 | import Error from "next/error"; 10 | 11 | const mapStateToProps = (state) => state; 12 | const mapDispatchToProps = { getCategoryPlaylists, clearReducer }; 13 | 14 | export default connect( 15 | mapStateToProps, 16 | mapDispatchToProps 17 | )(function SearchCategoryPage({ 18 | countryCode, 19 | categoryPage, 20 | token, 21 | getCategoryPlaylists, 22 | clearReducer, 23 | }) { 24 | const router = useRouter(); 25 | const id = router.query.id; 26 | 27 | useEffect(() => { 28 | if (!categoryPage?.items?.length) 29 | getCategoryPlaylists(id, token, countryCode, 25); 30 | 31 | return () => { 32 | clearReducer({}); 33 | }; 34 | }, [router]); 35 | 36 | if (categoryPage?.items === null && categoryPage?.msg === null) 37 | return ; 38 | 39 | return ( 40 | <> 41 | 42 | Spotify | {id || "Loading"} 43 | 44 |
    45 |
    46 |
    47 |

    {id}

    48 |
    49 |
    50 | 51 |
    52 |
    53 | 57 |
    58 |
    59 |
    60 | 61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /pages/profile/.module.sass: -------------------------------------------------------------------------------- 1 | .row_title 2 | text-transform: capitalize 3 | font-size: 20px 4 | font-weight: 700 5 | padding-right: 1.5rem 6 | margin-bottom: 0 7 | .row_span 8 | color: var(--bs-gray-500) 9 | font-size: 12px 10 | .profile_page_header 11 | height: 400px 12 | background: linear-gradient( to bottom, var(--bg_color_4) 0%, transparent 100%) 13 | .user_info_wrapper 14 | align-items: flex-end 15 | .user_cover_container 16 | width: 100% 17 | min-height: 247.5px 18 | background: var(--bg_color_4) 19 | box-shadow: 1px 1px 15px #00000050 20 | border: 10px solid var(--text_color) 21 | border-radius: 50% 22 | img 23 | border-radius: 50% 24 | overflow: hidden 25 | .user_details_container 26 | span 27 | font-size: 12px 28 | text-transform: uppercase 29 | letter-spacing: 1px 30 | font-weight: 500 31 | h1 32 | font-weight: 700 33 | font-size: 5rem 34 | overflow: hidden 35 | display: -webkit-box 36 | -webkit-line-clamp: 2 37 | -webkit-box-orient: vertical 38 | padding: .75rem 0 39 | .name_placeholder 40 | box-shadow: 1px 1px 15px #00000015 41 | display: flex 42 | height: 14px 43 | background: var(--bg_color_4) 44 | width: 150px 45 | @media screen and ( min-width: 1366px ) 46 | .col_xxl_3 47 | width: 25% 48 | .col_xxl_9 49 | width: 75% 50 | @media screen and ( max-width: 991px ) 51 | .profile_page_header 52 | .user_info_wrapper 53 | .user_details_container 54 | h1 55 | font-size: 4rem 56 | @media screen and ( max-width: 575px ) 57 | .profile_page_header 58 | .user_info_wrapper 59 | .user_details_container 60 | h1 61 | font-size: 3.5rem -------------------------------------------------------------------------------- /components/AppSidebar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | import AppLogo from "../../public/assets/icons/logo"; 4 | import AppIcon from "../../public/assets/icons/icon"; 5 | import ArrowLeft from "../../public/assets/icons/arrowLeft"; 6 | import ArrowRight from "../../public/assets/icons/arrowRight"; 7 | import DownloadIcon from "../../public/assets/icons/downloadIcon"; 8 | import AsideNavList from "../AsideNavList"; 9 | import Link from "next/link"; 10 | export default function AppSidebar({ style, open, setOpen }) { 11 | return ( 12 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /pages/search/[query].jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { connect } from "react-redux"; 3 | import { getSearchResults, clearReducer } from "../../redux/actions"; 4 | import { useRouter } from "next/router"; 5 | import { container } from "../../components/AppMain/.module.sass"; 6 | import PlaylistsRow from "../../components/PlaylistsRow"; 7 | import TrackComponent from "../../components/TrackComponent"; 8 | import { playlists_row_title } from "../../components/PlaylistsRow/.module.sass"; 9 | 10 | const mapStateToProps = (state) => state; 11 | const mapDispatchToProps = { getSearchResults, clearReducer }; 12 | export default connect( 13 | mapStateToProps, 14 | mapDispatchToProps 15 | )(function SearchQuery({ 16 | token, 17 | countryCode, 18 | getSearchResults, 19 | searchResults, 20 | clearReducer, 21 | }) { 22 | const router = useRouter(); 23 | const QUERY = router.query.query; 24 | 25 | useEffect(() => { 26 | if (QUERY) getSearchResults(token, QUERY, countryCode); 27 | 28 | return () => { 29 | clearReducer(); 30 | }; 31 | }, [QUERY]); 32 | 33 | return ( 34 |
    35 |
    36 |
    37 |

    Songs

    38 | {searchResults?.tracks?.items?.map((track, i) => ( 39 | 45 | ))} 46 |
    47 | 51 | 55 | 59 |
    60 |
    61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /redux/actions/api/getGenres.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | import FastAverageColor from "fast-average-color"; 3 | const fac = new FastAverageColor(); 4 | import rac from "randomcolor"; 5 | 6 | const getGenres = (token, country, limit, offset) => async (dispatch) => { 7 | try { 8 | const categories = await api.get("/browse/categories", { 9 | headers: { 10 | Authorization: `Bearer ${ 11 | token || window.localStorage.getItem("token") 12 | }`, 13 | }, 14 | params: { 15 | country: country || "EG", 16 | limit: limit || 5, 17 | offset: offset || 0, 18 | }, 19 | }); 20 | const ids = categories.data.categories.items.map((item) => item.id); 21 | ids.map(async (id, i) => { 22 | try { 23 | const res = await api.get(`/browse/categories/${id}/playlists`, { 24 | headers: { 25 | Authorization: `Bearer ${ 26 | token || window.localStorage.getItem("token") 27 | }`, 28 | }, 29 | params: { 30 | country: country || "EG", 31 | limit: 1, 32 | }, 33 | }); 34 | 35 | if (res.data.playlists.items.length) { 36 | const colorInfo = await fac.getColorAsync( 37 | res.data.playlists.items[0].images[0].url, 38 | { 39 | ignoredColor: [ 40 | [255, 255, 255, 255], 41 | [0, 0, 0, 255], 42 | ], 43 | } 44 | ); 45 | dispatch({ 46 | type: "GET__GENRES", 47 | payload: { 48 | id: ids[i], 49 | name: categories.data.categories.items[i].name, 50 | cover: res.data.playlists.items[0].images[0].url, 51 | bgColor: colorInfo.hex || rac(), 52 | }, 53 | }); 54 | } 55 | } catch (error) { 56 | dispatch({ 57 | type: "GET__GENRES", 58 | payload: { name: null, id: null, cover: null }, 59 | }); 60 | } 61 | }); 62 | } catch (error) { 63 | dispatch({ 64 | type: "GET__GENRES", 65 | payload: { name: null, id: null, cover: null }, 66 | }); 67 | } 68 | }; 69 | 70 | export default getGenres; 71 | -------------------------------------------------------------------------------- /components/LandingPage/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .app_landing_wrapper 3 | height: 100vh 4 | display: flex 5 | flex-direction: column 6 | background: var(--bg_color) 7 | overflow: hidden 8 | overflow-y: scroll 9 | &::-webkit-scrollbar 10 | display: none 11 | .hero_wrapper 12 | height: 100% 13 | display: flex 14 | align-items: center 15 | .connect_me 16 | margin-top: 3rem 17 | p 18 | font-size: 11px 19 | color: var(--bs-gray-600) 20 | max-width: 320px 21 | ul 22 | margin: 0 23 | margin-bottom: 1rem 24 | list-style: none 25 | padding: 0 26 | display: flex 27 | li 28 | margin-right: 1rem 29 | a 30 | font-size: 22px 31 | color: var(--text-color) !important 32 | .app_mode_toggle_btn 33 | all: unset 34 | cursor: pointer 35 | margin-left: 1.5rem 36 | svg 37 | width: 22px 38 | height: 22px 39 | .logo_app_link 40 | .nav_list 41 | z-index: 2 42 | width: 100% 43 | margin: 0 1rem !important 44 | list-style: none 45 | display: flex 46 | align-items: center 47 | justify-content: space-between 48 | a 49 | color: var(--text-color) 50 | text-transform: capitalize 51 | font-weight: 600 52 | .hero_head 53 | font-weight: 800 54 | font-size: 3rem 55 | .login_cta_btn_styled 56 | border: 0 57 | outline: none 58 | display: inline-flex 59 | color: #fff !important 60 | border-radius: 100px 61 | padding: 12px 32px 62 | text-transform: capitalize 63 | background: $main-color 64 | &:hover 65 | background: $gradient_2 66 | .spotify_icon 67 | // overflow: hidden 68 | max-height: 500px 69 | display: flex 70 | align-items: center 71 | justify-content: center 72 | // position: relative 73 | svg 74 | // display: none 75 | width: 400px 76 | height: 400px 77 | color: $main-color 78 | // transform: rotate(-30deg) translateY(-50%) 79 | 80 | -------------------------------------------------------------------------------- /public/assets/icons/vicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/AuthGuard/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useLayoutEffect, useState } from "react"; 2 | import { connect } from "react-redux"; 3 | import { useRouter } from "next/router"; 4 | import LandingPage from "../LandingPage"; 5 | import { updateTokenState, getMe, getUserCountry } from "../../redux/actions"; 6 | import LoaderWrapper from "../LoaderWrapper"; 7 | import axios from "axios"; 8 | import { 9 | CLIENT_ID, 10 | CLIENT_SECRET, 11 | REDIRECT_URI, 12 | TOKEN_ENDPOINT, 13 | } from "../../utils/spotifyLogin"; 14 | export const AuthGuard = ({ 15 | user, 16 | children, 17 | token, 18 | updateTokenState, 19 | getMe, 20 | getUserCountry, 21 | countryCode, 22 | }) => { 23 | const router = useRouter(); 24 | const [code, setCode] = useState(null); 25 | 26 | useEffect(() => { 27 | // if (typeof window !== "undefined") 28 | if (window.localStorage.getItem("token") !== "null") { 29 | updateTokenState(window.localStorage.getItem("token")); 30 | !countryCode ? getUserCountry() : null; 31 | } 32 | }, []); 33 | 34 | useEffect(() => { 35 | if (router.asPath.startsWith("/?code=")) { 36 | setCode(router.asPath.replace("/?code=", "")); 37 | } 38 | }, []); 39 | 40 | useEffect(() => { 41 | if (code) 42 | axios({ 43 | method: "post", 44 | url: TOKEN_ENDPOINT, 45 | data: `grant_type=authorization_code&redirect_uri=${REDIRECT_URI}&client_id=${CLIENT_ID}&code=${code}`, 46 | headers: { 47 | Accept: "application/json", 48 | "Content-Type": "application/x-www-form-urlencoded", 49 | Authorization: 50 | "Basic " + 51 | new Buffer(CLIENT_ID + ":" + CLIENT_SECRET).toString("base64"), 52 | }, 53 | }).then((response) => { 54 | updateTokenState(response.data.access_token); 55 | window.location.assign("/"); 56 | }); 57 | }, [code]); 58 | 59 | useEffect(() => { 60 | if (token) { 61 | window.localStorage.setItem("token", token); 62 | getMe(token); 63 | } 64 | }, [token]); 65 | 66 | if (!token) { 67 | return ; 68 | } else { 69 | if (user) { 70 | return children; 71 | } else { 72 | if (user === null) return ; 73 | if (user === false) return ; 74 | } 75 | } 76 | }; 77 | 78 | const mapStateToProps = (state) => state; 79 | const mapDispatchToProps = { updateTokenState, getMe, getUserCountry }; 80 | export default connect(mapStateToProps, mapDispatchToProps)(AuthGuard); 81 | -------------------------------------------------------------------------------- /redux/actions/api/getWorkDetails.js: -------------------------------------------------------------------------------- 1 | import api from "../../../utils/api"; 2 | import FastAverageColor from "fast-average-color"; 3 | const fac = new FastAverageColor(); 4 | 5 | const getWorkDetails = (token, type, id, country) => async (dispatch) => { 6 | try { 7 | const res = await api.get(`/${type}s/${id}`, { 8 | headers: { 9 | Authorization: `Bearer ${ 10 | token || window.localStorage.getItem("token") 11 | }`, 12 | }, 13 | params: { 14 | market: country || "EG", 15 | }, 16 | }); 17 | const colorInfo = await fac.getColorAsync(res.data.images[0].url, { 18 | ignoredColor: [ 19 | [255, 255, 255, 255], 20 | [0, 0, 0, 255], 21 | ], 22 | }); 23 | if (type !== "playlist") { 24 | const artist = type === "album" ? res.data.artists[0].id : res.data.id; 25 | const moreAlbums = await api.get(`/artists/${artist}/albums`, { 26 | headers: { 27 | Authorization: `Bearer ${ 28 | token || window.localStorage.getItem("token") 29 | }`, 30 | }, 31 | params: { 32 | limit: 5, 33 | }, 34 | }); 35 | const moreArtists = await api.get(`/artists/${artist}/related-artists`, { 36 | headers: { 37 | Authorization: `Bearer ${ 38 | token || window.localStorage.getItem("token") 39 | }`, 40 | }, 41 | params: { 42 | country: country || "EG", 43 | }, 44 | }); 45 | const moreTracks = await api.get(`/artists/${artist}/top-tracks`, { 46 | headers: { 47 | Authorization: `Bearer ${ 48 | token || window.localStorage.getItem("token") 49 | }`, 50 | }, 51 | params: { 52 | country: country || "EG", 53 | }, 54 | }); 55 | dispatch({ 56 | type: "GET_PLAYLIST|ARTIST|ALBUM__VIEW", 57 | payload: { 58 | ...res.data, 59 | bgColor: colorInfo.hex, 60 | moreAlbums: moreAlbums.data.items || [], 61 | moreArtists: moreArtists.data.artists || [], 62 | moreTracks: moreTracks.data.tracks || [], 63 | }, 64 | }); 65 | } else { 66 | dispatch({ 67 | type: "GET_PLAYLIST|ARTIST|ALBUM__VIEW", 68 | payload: { 69 | ...res.data, 70 | bgColor: colorInfo.hex, 71 | }, 72 | }); 73 | } 74 | } catch (error) { 75 | dispatch({ 76 | type: "GET_PLAYLIST|ARTIST|ALBUM__VIEW", 77 | payload: null, 78 | }); 79 | } 80 | }; 81 | 82 | export default getWorkDetails; 83 | -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect } from "react"; 2 | import { connect } from "react-redux"; 3 | import PlaylistsRow from "../components/PlaylistsRow"; 4 | import { container } from "../components/AppMain/.module.sass"; 5 | import { 6 | getFeaturedList, 7 | getRecentlyPlayedLists, 8 | getTopLikedArtists, 9 | getTopLikedTracks, 10 | getNewReleases, 11 | getBrowseCategories, 12 | } from "../redux/actions"; 13 | 14 | const mapStateToProps = (state) => state; 15 | const mapDispatchToProps = { 16 | getFeaturedList, 17 | getRecentlyPlayedLists, 18 | getTopLikedArtists, 19 | getTopLikedTracks, 20 | getNewReleases, 21 | getBrowseCategories, 22 | }; 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(function Index({ 27 | featuredPlaylists, 28 | recentlyPlaylists, 29 | getFeaturedList, 30 | getRecentlyPlayedLists, 31 | getTopLikedArtists, 32 | artistsPlaylists, 33 | token, 34 | getTopLikedTracks, 35 | albumsPlaylists, 36 | newReleasePlaylists, 37 | getNewReleases, 38 | countryCode, 39 | getBrowseCategories, 40 | browseCategories, 41 | }) { 42 | useLayoutEffect(() => { 43 | if (!recentlyPlaylists?.items?.length) getRecentlyPlayedLists(token); 44 | if (!featuredPlaylists?.items?.length) getFeaturedList(token, countryCode); 45 | if (!artistsPlaylists?.items?.length) getTopLikedArtists(token); 46 | if (!albumsPlaylists?.items?.length) getTopLikedTracks(token); 47 | if (!newReleasePlaylists?.items?.length) getNewReleases(token, countryCode); 48 | if (!browseCategories.length) getBrowseCategories(token, countryCode, 5, 0); 49 | }, []); 50 | return ( 51 |
    52 |
    53 | 58 | 63 | 68 | 73 | 78 | {browseCategories.map((category, i) => { 79 | return ; 80 | })} 81 |
    82 |
    83 | ); 84 | }); 85 | -------------------------------------------------------------------------------- /components/TrackComponent/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .track_component_row 3 | border-radius: 5px 4 | color: var(--bs-gray-600) 5 | .play_pause_btn 6 | color: var(--text_color) !important 7 | display: none 8 | &:hover 9 | .track_index_num 10 | display: none 11 | .play_pause_btn 12 | display: inline-block !important 13 | button 14 | background: transparent 15 | box-shadow: none 16 | svg 17 | transform: 0 18 | margin: 0 !important 19 | width: 30px 20 | height: 30px 21 | color: var(--text_color) 22 | background: var(--bg_color_4) 23 | .track_artist_link,.track_album_link,.track_added_at_date,.track_index 24 | color: var(--text_color) !important 25 | &.is_playing 26 | background: var(--bg_color_4) 27 | .track_artist_link,.track_album_link,.track_added_at_date,.track_index 28 | color: var(--text_color) !important 29 | .track_name,.track_index 30 | color: $main-color !important 31 | .track_index 32 | min-width: 30px 33 | font-weight: 600 34 | .track_title 35 | display: inline-flex 36 | justify-content: space-between 37 | align-items: center 38 | .track_cover_wrapper 39 | display: grid 40 | place-items: center 41 | box-shadow: 1px 1px 5px #00000020 42 | background: var(--bg_color_4) 43 | height: 40px !important 44 | width: 40px !important 45 | .track_name_artist 46 | display: flex 47 | flex-direction: column 48 | justify-content: space-between 49 | .track_name 50 | overflow: hidden 51 | display: -webkit-box 52 | -webkit-line-clamp: 1 53 | -webkit-box-orient: vertical 54 | color: var(--text_color) 55 | font-weight: 500 56 | ._playlist_placeholder, 57 | .icon_playlist_placeholder 58 | box-shadow: 1px 1px 5px #00000020 59 | display: inline-flex 60 | background: var(--bg_color_4) 61 | height: 14px 62 | width: 100px 63 | .icon_playlist_placeholder 64 | display: inline-flex 65 | width: 14px 66 | .track_artist_link, 67 | .track_album_link 68 | display: inline-flex 69 | color: var(--bs-gray-600) 70 | &:not(:last-of-type) 71 | margin-right: 5px 72 | &::after 73 | content: ', ' 74 | .track_artist_link 75 | font-size: 12px 76 | 77 | 78 | -------------------------------------------------------------------------------- /public/assets/icons/verified.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function VerifiedIcon() { 4 | return ( 5 | <> 6 | {/* */} 7 | 8 | 15 | 22 | 23 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /pages/[work]/[id]/.module.sass: -------------------------------------------------------------------------------- 1 | .work_page_content_wrapper 2 | height: 500px 3 | background: linear-gradient( to bottom, var(--bg_color_4) 0%, transparent 100%) 4 | .work_info_wrapper 5 | align-items: flex-end 6 | .work_cover_container 7 | width: 100% 8 | min-height: 247.5px 9 | background: var(--bg_color_4) 10 | box-shadow: 1px 1px 15px #00000050 11 | img 12 | &.circled 13 | border-radius: 50% 14 | img 15 | border-radius: 50% 16 | overflow: hidden 17 | .work_details_container 18 | span 19 | font-size: 12px 20 | text-transform: uppercase 21 | letter-spacing: 1px 22 | font-weight: 500 23 | a 24 | color: var(--text-color) 25 | h1 26 | font-weight: 700 27 | font-size: 5rem 28 | overflow: hidden 29 | display: -webkit-box 30 | -webkit-line-clamp: 2 31 | -webkit-box-orient: vertical 32 | padding: .75rem 0 33 | .work_description 34 | display: block 35 | margin-bottom: 0.25rem 36 | color: var(--text-color) 37 | .work_fact 38 | margin: 0 39 | font-weight: 300 40 | font-size: 13px 41 | .work_type 42 | display: flex 43 | align-items: inline-center 44 | line-height: 22px 45 | svg 46 | margin-right: 0.5rem 47 | font-size: 22px 48 | fill: var(--bs-blue) 49 | .type_placeholder, 50 | .name_placeholder, 51 | .more_placeholder 52 | box-shadow: 1px 1px 15px #00000015 53 | display: flex 54 | height: 14px 55 | background: var(--bg_color_4) 56 | width: 150px 57 | .name_placeholder 58 | width: 500px 59 | height: 3rem 60 | margin: 1.5rem 0 61 | .more_placeholder 62 | height: 18px 63 | margin: 0.7rem 0 64 | width: 350px 65 | &:last-of-type 66 | margin-top: 1.5rem 67 | margin-bottom: 0 68 | .work_actions_wrapper 69 | display: flex 70 | @media screen and ( min-width: 1366px ) 71 | .col_xxl_3 72 | width: 25% 73 | .col_xxl_9 74 | width: 75% 75 | @media screen and ( max-width: 991px ) 76 | .work_page_content_wrapper 77 | .work_info_wrapper 78 | .work_details_container 79 | h1 80 | font-size: 4rem 81 | @media screen and ( max-width: 575px ) 82 | .work_page_content_wrapper 83 | .work_info_wrapper 84 | .work_details_container 85 | h1 86 | font-size: 3.5rem -------------------------------------------------------------------------------- /pages/genre/[genre].jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { connect } from "react-redux"; 3 | import { useRouter } from "next/router"; 4 | import PlaylistsRow from "../../components/PlaylistsRow"; 5 | import Head from "next/head"; 6 | import capitalize from "../../utils/capitalize"; 7 | import { container } from "../../components/AppMain/.module.sass"; 8 | import { 9 | getRecentlyPlayedLists, 10 | getFeaturedList, 11 | getTopLikedArtists, 12 | getTopLikedTracks, 13 | getNewReleases, 14 | clearReducer, 15 | } from "../../redux/actions"; 16 | 17 | const mapStateToProps = (state) => state; 18 | const mapDispatchToProps = { 19 | getRecentlyPlayedLists, 20 | clearReducer, 21 | getFeaturedList, 22 | getNewReleases, 23 | getTopLikedArtists, 24 | getTopLikedTracks, 25 | }; 26 | 27 | export default connect( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(function GenrePage({ 31 | genrePlaylists, 32 | getNewReleases, 33 | countryCode, 34 | getRecentlyPlayedLists, 35 | getFeaturedList, 36 | token, 37 | clearReducer, 38 | getTopLikedArtists, 39 | getTopLikedTracks, 40 | }) { 41 | const router = useRouter(); 42 | 43 | useEffect(() => { 44 | if (!genrePlaylists?.items?.length) 45 | switch (router.query.genre) { 46 | case "recently_played": 47 | getRecentlyPlayedLists(token, 30); 48 | break; 49 | case "featured_playlists": 50 | getFeaturedList(token, countryCode, 30); 51 | break; 52 | case "top_artists": 53 | getTopLikedArtists(token, 30); 54 | break; 55 | case "top_albums": 56 | getTopLikedTracks(token, 30); 57 | break; 58 | case "new_releases": 59 | getNewReleases(token, countryCode, 30); 60 | break; 61 | default: 62 | break; 63 | } 64 | return () => { 65 | clearReducer({ msg: null, items: [] }); 66 | }; 67 | }, []); 68 | return ( 69 | <> 70 | 71 | {`Spotify App | ${ 72 | genrePlaylists?.msg ? capitalize(genrePlaylists?.msg) : "Loading" 73 | }`} 74 | 75 |
    76 |
    77 | 78 |
    79 |
    80 | 81 | ); 82 | }); 83 | 84 | export async function getStaticProps(context) { 85 | return { 86 | props: {}, 87 | notFound: ![ 88 | "recently_played", 89 | "featured_playlists", 90 | "top_albums", 91 | "top_artists", 92 | "new_releases", 93 | ].includes(context?.params?.genre), 94 | }; 95 | } 96 | export async function getStaticPaths() { 97 | return { 98 | paths: [ 99 | { params: { genre: "recently_played" } }, 100 | { params: { genre: "featured_playlists" } }, 101 | { params: { genre: "top_albums" } }, 102 | { params: { genre: "top_artists" } }, 103 | { params: { genre: "new_releases" } }, 104 | ], 105 | fallback: false, 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /components/PlayListComponent/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | 3 | .playlist_component_styled 4 | display: flex 5 | flex-direction: column 6 | margin: 1.25rem 0 7 | padding: 15px 8 | background: var(--bg_color_3) 9 | border-radius: 6px 10 | min-height: 190px 11 | box-shadow: 1px 1px 10px #00000015 12 | &:hover 13 | background: var(--bg_color_4) 14 | .playlist_cover_wrapper 15 | .playlist_play_pause_btn_container 16 | visibility: visible !important 17 | bottom: 10px !important 18 | &.circled 19 | .playlist_play_pause_btn_container 20 | bottom: 0px !important 21 | a 22 | color: var(--text-color) 23 | .playlist_cover_wrapper 24 | box-shadow: 1px 1px 10px #00000025 25 | background: var(--bg_color_2) 26 | width: 100% 27 | min-height: 158px 28 | border-radius: 6px 29 | margin-bottom: 1.25rem 30 | position: relative 31 | img 32 | overflow: hidden 33 | border-radius: 6px !important 34 | &.circled 35 | align-self: center 36 | max-width: 158px 37 | max-height: 122px !important 38 | border-radius: 50% 39 | justify-content: center 40 | align-items: center 41 | img 42 | border-radius: 50% !important 43 | .playlist_play_pause_btn_container 44 | &.isPlaying 45 | bottom: 0 !important 46 | .playlist_cover_link 47 | width: 100% !important 48 | min-height: 100% !important 49 | img 50 | user-select: none !important 51 | pointer-events: none !important 52 | .playlist_play_pause_btn_container 53 | z-index: 2 54 | position: absolute 55 | border: 0 56 | bottom: 0 57 | right: 10px 58 | visibility: hidden 59 | transition: bottom .3s linear 60 | &.isPlaying 61 | visibility: visible !important 62 | bottom: 10px !important 63 | .playlist_title 64 | overflow: hidden 65 | display: -webkit-box 66 | -webkit-line-clamp: 1 67 | -webkit-box-orient: vertical 68 | .playlist_overview 69 | margin: 0 70 | color: #B3B3B3 71 | font-size: 12px 72 | overflow: hidden 73 | display: -webkit-box 74 | -webkit-line-clamp: 2 75 | -webkit-box-orient: vertical 76 | min-height: 38px 77 | .skelton_placeholder 78 | width: 100px 79 | background: var(--bg_color_2) 80 | height: 12px 81 | display: block 82 | box-shadow: 1px 1px 2px #00000015 83 | @media screen and ( min-width:1366px ) 84 | .col_xxl_2_5 85 | width: 20% 86 | @media screen and ( max-width: 991px ) 87 | .playlist_component_styled 88 | .playlist_cover_wrapper 89 | .playlist_play_pause_btn_container 90 | visibility: visible !important 91 | bottom: 10px !important 92 | &.circled 93 | .playlist_play_pause_btn_container 94 | bottom: 0px !important -------------------------------------------------------------------------------- /components/AudioPlayer/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | 3 | .audio_playback_control_wrapper 4 | border-top: 2px solid var(--bg_color_3) 5 | display: flex 6 | justify-content: space-between 7 | align-items: center 8 | button 9 | all: unset 10 | cursor: pointer 11 | font-size: 22px 12 | color: var(--bs-gray-600) 13 | margin: 0 .75rem 14 | &:hover 15 | color: var(--text_color) 16 | &.active 17 | color: $main-color 18 | ._wrapper 19 | width: 100% 20 | padding-left: 15px 21 | padding-right: 15px 22 | // display: flex 23 | // justify-content: stretch 24 | // justify-content: center 25 | 26 | // max-width: 450px 27 | .current_track_wrapper, 28 | .track_progress_control_wrapper, 29 | .track_adds_settings 30 | display: flex 31 | align-items: center 32 | .track_adds_settings 33 | justify-content: flex-end 34 | .current_track_wrapper 35 | .track_cover 36 | display: grid 37 | place-items: center 38 | background: var(--bg_color_3) 39 | box-shadow: 1px 1px 10px #00000080 40 | width: 45px 41 | height: 45px 42 | .track_title_artist_col 43 | padding: 0 18px 44 | display: flex 45 | flex-direction: column 46 | .track_title 47 | margin-bottom: 0rem 48 | overflow: hidden 49 | display: -webkit-box 50 | -webkit-line-clamp: 1 51 | -webkit-box-orient: vertical 52 | .track_artist 53 | font-size: 11px 54 | font-weight: 500 55 | color: var(--bs-gray-600) 56 | overflow: hidden 57 | display: -webkit-box 58 | -webkit-line-clamp: 1 59 | -webkit-box-orient: vertical 60 | .track_progress_control_wrapper 61 | width: 100% 62 | height: 90px 63 | justify-content: space-evenly 64 | flex-direction: column 65 | max-width: 450px 66 | .play_pause_btn 67 | background: var(--text_color) 68 | width: 30px 69 | height: 30px 70 | border-radius: 50% 71 | position: relative 72 | svg 73 | position: absolute 74 | left: 50% 75 | top: 50% 76 | transform: translate(calc( -50% + 6%),-50%) 77 | width: 90% 78 | height: 90% 79 | color: var(--bg_color_3) 80 | &.isPlaying 81 | svg 82 | transform: translate(calc( -50% ),-50%) 83 | .track_interval 84 | font-size: 11px 85 | min-width: 50px 86 | text-align: center 87 | .volume_level 88 | font-size: 11px 89 | min-width: 60px 90 | text-align: center 91 | @media screen and ( max-width: 767px ) 92 | .audio_playback_control_wrapper 93 | ._wrapper 94 | width: auto 95 | @media screen and ( max-width: 480px ) 96 | .audio_playback_control_wrapper 97 | button 98 | margin: 0 .5rem 99 | -------------------------------------------------------------------------------- /components/TracksTable/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import styles from "./.module.sass"; 3 | import { BiTimeFive } from "react-icons/bi"; 4 | import { container } from "../AppMain/.module.sass"; 5 | import TrackComponent from "../TrackComponent"; 6 | 7 | export default function TracksTable({ tracks, type, link, image, uri }) { 8 | const tableHead = useRef(); 9 | const [isTopFixed, setisTopFixed] = useState(false); 10 | useEffect(() => { 11 | const observer = new IntersectionObserver( 12 | ([e]) => { 13 | if (e.intersectionRect.top === 75) setisTopFixed(true); 14 | else setisTopFixed(false); 15 | }, 16 | { 17 | rootMargin: "-75px 0px 0px 0px", 18 | threshold: [1], 19 | } 20 | ); 21 | observer.observe(tableHead.current); 22 | }, []); 23 | 24 | return ( 25 |
    26 |
    32 |
    33 |
    34 |
    35 | {tracks?.length ? ( 36 | 37 | #title 38 | 39 | ) : ( 40 | <> 41 | 44 | 45 | 46 | )} 47 |
    48 |
    49 | {tracks?.length && type === "playlist" ? ( 50 | album 51 | ) : type === "playlist" ? ( 52 | 53 | ) : null} 54 |
    55 |
    56 | {tracks?.length && type === "playlist" ? ( 57 | data added 58 | ) : type === "playlist" ? ( 59 | 60 | ) : null} 61 |
    62 |
    63 | 64 | {tracks?.length ? ( 65 | 66 | ) : ( 67 | 68 | )} 69 | 70 |
    71 |
    72 |
    73 |
    74 |
    75 |
    76 | {tracks?.length 77 | ? tracks?.map((track, i) => ( 78 | 86 | )) 87 | : [...Array(15)].map((_, i) => ( 88 | 89 | ))} 90 |
    91 |
    92 |
    93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /components/PlayListComponent/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styles from "./.module.sass"; 3 | import Link from "next/link"; 4 | import NextImage from "../NextImage"; 5 | import PlayPauseBtn from "../PlayPauseBtn"; 6 | import { connect } from "react-redux"; 7 | 8 | export default connect( 9 | (state) => state, 10 | {} 11 | )(function PlayListComponent({ playlist, spotifyPlayer }) { 12 | const [isPlaying, setisPlaying] = useState(false); 13 | 14 | useEffect(() => { 15 | if (spotifyPlayer) { 16 | if ( 17 | spotifyPlayer?.context?.uri === playlist?.uri && 18 | !spotifyPlayer?.paused 19 | ) { 20 | setisPlaying(true); 21 | } else { 22 | setisPlaying(false); 23 | } 24 | } 25 | }, [spotifyPlayer, playlist]); 26 | 27 | return ( 28 |
    29 |
    30 |
    35 | 36 | 37 | {playlist ? ( 38 | 48 | ) : null} 49 | 50 | 51 | {playlist ? ( 52 |
    57 | 63 |
    64 | ) : null} 65 |
    66 | {playlist ? ( 67 | 68 | 69 |
    {playlist.name}
    70 |

    87 | 88 | 89 | ) : ( 90 | <> 91 | 92 | 93 | 94 | 95 | )} 96 |

    97 |
    98 | ); 99 | }); 100 | -------------------------------------------------------------------------------- /components/AsideNavList/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SearchIcon from "../../public/assets/icons/searchIcon"; 3 | import HomeIcon from "../../public/assets/icons/homeIcon"; 4 | import LibIcon from "../../public/assets/icons/LibIcon"; 5 | // import HeartIcon from "../../public/assets/icons/HeartIcon"; 6 | import PlusIcon from "../../public/assets/icons/plusIcon"; 7 | import AppModeIcon from "../../public/assets/icons/appModeIcon"; 8 | import styles from "./.module.sass"; 9 | import NavLink from "../NavLink"; 10 | import { useTheme } from "next-themes"; 11 | import capitalize from "../../utils/capitalize"; 12 | 13 | export default function AsideNavList({ open }) { 14 | const { theme, setTheme } = useTheme(""); 15 | const siteMap = [ 16 | { 17 | name: "home", 18 | path: "/", 19 | icon: , 20 | route: true, 21 | }, 22 | { 23 | name: "search", 24 | path: "/search", 25 | icon: , 26 | route: true, 27 | }, 28 | // { 29 | // name: "library", 30 | // path: "/collection", 31 | // icon: , 32 | // route: true, 33 | // }, 34 | // {}, 35 | // { 36 | // name: "create playlist", 37 | // path: "/play", 38 | // icon: , 39 | // }, 40 | // { 41 | // name: "liked songs", 42 | // path: "/collection/tracks", 43 | // icon: , 44 | // route: true, 45 | // }, 46 | // {}, 47 | { 48 | name: `${theme === "dark" ? "light" : "dark"} mode`, 49 | path: "/mode", 50 | icon: , 51 | function: setTheme, 52 | }, 53 | ]; 54 | 55 | return ( 56 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /components/AppMain/.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables' 2 | .app_main_area 3 | overflow: hidden !important 4 | width: 100vw !important 5 | height: 100vh !important 6 | display: flex !important 7 | flex-direction: column !important 8 | .app_main_top_section 9 | z-index: 1 10 | display: flex 11 | height: calc( 100vh - 90px ) 12 | position: relative 13 | .app_main_sidebar 14 | box-shadow: 1px 1px 5px #00000015 15 | overflow: hidden 16 | overflow-y: scroll 17 | &::-webkit-scrollbar 18 | display: none 19 | width: 90px 20 | display: flex 21 | flex-direction: column 22 | justify-content: space-between 23 | background: var(--bg_color) 24 | transition: $aside_transition 25 | z-index: 2 26 | .app_main_func_container 27 | scroll-behavior: smooth 28 | z-index: 1 29 | width: calc( 100% - 90px) 30 | background: var(--bg_color_2) 31 | overflow-y: scroll 32 | &::-webkit-scrollbar 33 | display: none 34 | width: 5px 35 | &::-webkit-scrollbar-track 36 | background: var(--bg_color_1) 37 | &::-webkit-scrollbar-thumb 38 | background: var(--bs-gray-400) 39 | .container 40 | padding-left: 32px 41 | padding-right: 32px 42 | width: 100% 43 | max-width: 1440px 44 | .scroll_top_btn 45 | all: unset 46 | background: var(--bg_color_4) 47 | color: var(--text_color) 48 | box-shadow: 1px 1px 10px #0000002f 49 | width: 36px 50 | height: 36px 51 | display: grid 52 | place-items: center 53 | border-radius: 4px 54 | cursor: pointer 55 | position: fixed 56 | right: 32px 57 | bottom: 122px 58 | z-index: 10 59 | animation: toTop .5s 60 | svg 61 | width: 18px 62 | height: 18px 63 | &:hover 64 | background: $main-color 65 | color: #fff 66 | .loading_more_spinner 67 | display: flex 68 | justify-content: center 69 | align-content: center 70 | text-align: center 71 | .grow_spinner 72 | margin-bottom: 2rem 73 | .app_main_bottom_section 74 | z-index: 2 75 | box-shadow: 1px 1px 5px #00000015 76 | background: var(--bg_color_1); 77 | height: 90px !important 78 | position: fixed 79 | left: 0 80 | bottom: 0 81 | width: 100% 82 | .aside_open 83 | .app_main_sidebar 84 | transition: 0s !important 85 | width: 240px !important 86 | .app_main_func_container 87 | width: calc( 100% - 240px) !important 88 | 89 | @keyframes toTop 90 | 0% 91 | bottom: 80px 92 | 100% 93 | bottom: 122px 94 | 95 | 96 | @media screen and ( max-width: 991px ) 97 | .app_main_area 98 | .app_main_top_section 99 | .app_main_sidebar 100 | width: 70px !important 101 | .app_main_func_container 102 | width: calc( 100% - 70px) !important 103 | @media screen and ( max-width: 575px ) 104 | .app_main_area 105 | .app_main_top_section 106 | .app_main_sidebar 107 | width: 60px !important 108 | .app_main_func_container 109 | width: calc( 100% - 60px) !important 110 | .container 111 | padding-left: 16px 112 | padding-right: 16px 113 | .scroll_top_btn 114 | right: 16px 115 | 116 | -------------------------------------------------------------------------------- /pages/profile/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { connect } from "react-redux"; 3 | import { container } from "../../components/AppMain/.module.sass"; 4 | import { getUserTopItems } from "../../redux/actions"; 5 | import TrackComponent from "../../components/TrackComponent"; 6 | import PlaylistsRow from "../../components/PlaylistsRow"; 7 | import styles from "./.module.sass"; 8 | import NextImage from "../../components/NextImage"; 9 | import FastAverageColor from "fast-average-color"; 10 | const fac = new FastAverageColor(); 11 | 12 | const mapStateToProps = (state) => state; 13 | const mapDispatchToProps = { getUserTopItems }; 14 | 15 | export const UserProfile = ({ user, token, getUserTopItems, userTopItems }) => { 16 | const [colorInfo, setcolorInfo] = useState(); 17 | useEffect(() => { 18 | if (user?.images[0]?.url) { 19 | fac 20 | .getColorAsync(user?.images[0]?.url, { 21 | ignoredColor: [ 22 | [255, 255, 255, 255], 23 | [0, 0, 0, 255], 24 | ], 25 | }) 26 | .then((res) => { 27 | setcolorInfo(res); 28 | }); 29 | } 30 | }, [user]); 31 | 32 | useEffect(() => { 33 | !userTopItems?.tracks?.length || !userTopItems?.artists?.length 34 | ? getUserTopItems(token, 10) 35 | : null; 36 | }, []); 37 | 38 | return ( 39 |
    45 |
    46 |
    49 |
    52 |
    53 | {user?.images?.length ? ( 54 | 64 | ) : null} 65 |
    66 |
    67 |
    70 |
    71 | PROFILE 72 | {user?.display_name ? ( 73 |

    {user?.display_name}

    74 | ) : ( 75 | 76 | )} 77 |
    78 |
    79 |
    80 |
    81 |
    82 |
    83 |
    84 |

    Top artists this month

    85 | Only visible to you 86 | 93 |
    94 |
    95 |
    96 |
    97 |

    Top tracks this month

    98 | Only visible to you 99 | {userTopItems?.tracks?.map((track, i) => ( 100 | 107 | ))} 108 |
    109 |
    110 |
    111 |
    112 | ); 113 | }; 114 | 115 | export default connect(mapStateToProps, mapDispatchToProps)(UserProfile); 116 | -------------------------------------------------------------------------------- /pages/search/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Head from "next/head"; 3 | import styles from "./.module.sass"; 4 | import { getGenres } from "../../redux/actions"; 5 | import { connect } from "react-redux"; 6 | import CategoryComponent from "../../components/CategoryComponent"; 7 | import { Swiper, SwiperSlide } from "swiper/react"; 8 | import { Navigation } from "swiper"; 9 | import { BsChevronLeft, BsChevronRight } from "react-icons/bs"; 10 | import { container } from "../../components/AppMain/.module.sass"; 11 | 12 | import "swiper/css"; 13 | import "swiper/css/navigation"; 14 | 15 | const mapStateToProps = (state) => state; 16 | const mapDispatchToProps = { getGenres }; 17 | 18 | export default connect( 19 | mapStateToProps, 20 | mapDispatchToProps 21 | )(function SearchPage({ token, countryCode, getGenres, allCategories }) { 22 | useEffect(() => { 23 | if (!allCategories.length) getGenres(token, countryCode, 40); 24 | }, []); 25 | 26 | return ( 27 | <> 28 | 29 | Spotify App | Search 30 | 31 |
    32 |
    33 |
    34 |
    35 |

    your top categories

    36 |
    39 |
    42 | 43 |
    44 |
    47 | 48 |
    49 |
    50 | <> 51 | 74 | {!allCategories.length 75 | ? [...Array(5)].map((el, i) => { 76 | return ( 77 | 78 | 79 | 80 | ); 81 | }) 82 | : allCategories?.slice(0, 5).map((el, i) => ( 83 | 84 | 85 | 86 | ))} 87 | 88 | 89 |
    90 |
    91 |
    92 |
    93 |

    browse all

    94 | {allCategories.length 95 | ? allCategories.slice(6).map((el, i) => { 96 | return ( 97 | 98 | ); 99 | }) 100 | : [...Array(20)].map((el, i) => { 101 | return ; 102 | })} 103 |
    104 |
    105 |
    106 |
    107 | 108 | ); 109 | }); 110 | -------------------------------------------------------------------------------- /components/AppMain/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useLayoutEffect, useEffect } from "react"; 2 | import styles from "./.module.sass"; 3 | import AppSidebar from "../AppSidebar"; 4 | import SignupBanner from "../SignupBanner"; 5 | import ActionsTopBar from "../ActionsTopBar"; 6 | import { useRouter } from "next/router"; 7 | import { FiChevronUp } from "react-icons/fi"; 8 | import { getBrowseCategories } from "../../redux/actions"; 9 | import { connect } from "react-redux"; 10 | import AudioPlayer from "../AudioPlayer"; 11 | import { setSpotifyPlayer, getPlayerState } from "../../redux/actions"; 12 | 13 | export default connect((state) => state, { 14 | getBrowseCategories, 15 | setSpotifyPlayer, 16 | getPlayerState, 17 | })(function AppMain({ 18 | user, 19 | token, 20 | countryCode, 21 | children, 22 | getBrowseCategories, 23 | }) { 24 | //************************************************************// 25 | const [scrollBtn, setscrollBtn] = useState(false); 26 | const [isSticky, setisSticky] = useState(false); 27 | const [open, setOpen] = useState(false); 28 | const router = useRouter(); 29 | const container = useRef(null); 30 | const [categoriesPerRender, _] = useState(1); 31 | const [offset, setOffset] = useState(5); 32 | 33 | useEffect(() => { 34 | container.current.scrollTop = 0; 35 | }, [router]); 36 | 37 | useLayoutEffect(() => { 38 | if (typeof window !== "undefined") 39 | if (window.innerWidth < 991) setOpen(false); 40 | else 41 | setOpen( 42 | JSON.parse(window.localStorage.getItem("isAsideOpen")) || false 43 | ); 44 | }, []); 45 | 46 | if (typeof window !== "undefined") { 47 | window.onresize = () => { 48 | if (window.innerWidth < 991) { 49 | if (open) setOpen(false); 50 | } else { 51 | if (!open) setOpen(true); 52 | } 53 | }; 54 | if (container.current) 55 | container.current.onscroll = () => { 56 | if (router.pathname === "/") { 57 | if ( 58 | container?.current?.scrollHeight - 59 | container?.current?.offsetHeight === 60 | container?.current?.scrollTop 61 | ) { 62 | if (categoriesPerRender * (offset + 1) <= 40) loadMoreCategories(); 63 | } 64 | } 65 | if (container?.current?.scrollTop >= 200) { 66 | setisSticky(true); 67 | setscrollBtn(true); 68 | } 69 | if (container?.current?.scrollTop < 100) { 70 | setscrollBtn(false); 71 | setisSticky(false); 72 | } 73 | }; 74 | } 75 | 76 | const loadMoreCategories = () => { 77 | setOffset(offset + 1); 78 | getBrowseCategories( 79 | token, 80 | countryCode, 81 | categoriesPerRender, 82 | categoriesPerRender * (offset + 1) 83 | ); 84 | }; 85 | 86 | return ( 87 |
    90 |
    91 | 96 |
    97 | 98 |
    99 | <> 100 | {scrollBtn ? ( 101 | 108 | ) : null} 109 | 110 |
    111 | {children} 112 | {router.pathname === "/" && 113 | categoriesPerRender * (offset + 1) <= 41 ? ( 114 |
    115 |
    116 |
    120 | Loading... 121 |
    122 |
    123 |
    124 | ) : null} 125 |
    126 |
    127 |
    128 | {user?.product !== "premium" ? : } 129 |
    130 |
    131 | ); 132 | }); 133 | -------------------------------------------------------------------------------- /components/LandingPage/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./.module.sass"; 3 | import Link from "next/link"; 4 | import AppLogo from "../../public/assets/icons/logo"; 5 | import { useRouter } from "next/router"; 6 | import { SPOTIFY_LOGIN } from "../../utils/spotifyLogin"; 7 | import { useTheme } from "next-themes"; 8 | import AppModeIcon from "../../public/assets/icons/appModeIcon"; 9 | import { BsSpotify } from "react-icons/bs"; 10 | import { 11 | AiFillFacebook, 12 | AiFillGithub, 13 | AiFillLinkedin, 14 | AiFillSkype, 15 | AiOutlineGlobal, 16 | } from "react-icons/ai"; 17 | 18 | export default function LandingPage() { 19 | const router = useRouter(); 20 | const { theme, setTheme } = useTheme(""); 21 | 22 | return ( 23 |
    24 |
    25 |
    26 |
    27 | 28 | 29 | 30 | 31 | 32 |
    33 |
    34 | 65 | 74 |
    75 |
    76 |
    77 |
    78 |
    79 |
    80 |
    81 |

    82 | Music you love, right at your fingertrips. 83 |

    84 |

    85 | Ad-free, offline listening and more for $9.99/month, Cancel 86 | anytime 87 |

    88 | 97 |
    98 |

    99 | App still in development mode. users are limited. contact me 100 | with your spotify email and user to add you 101 |

    102 | 123 |
    124 |
    125 |
    126 | 127 |
    128 |
    129 |
    130 |
    131 |
    132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /components/TrackComponent/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styles from "./.module.sass"; 3 | import Link from "next/link"; 4 | import moment from "moment"; 5 | import NextImage from "../NextImage"; 6 | import { connect } from "react-redux"; 7 | import PlayPauseBtn from "../PlayPauseBtn"; 8 | 9 | export default connect((state) => state)(function TrackComponent({ 10 | spotifyPlayer, 11 | track, 12 | type, 13 | link, 14 | image, 15 | uri, 16 | }) { 17 | const [isPlaying, setisPlaying] = useState(false); 18 | 19 | useEffect(() => { 20 | if (spotifyPlayer) { 21 | if ( 22 | (spotifyPlayer?.track_window?.current_track?.uri === 23 | track?.track?.uri || 24 | spotifyPlayer?.track_window?.current_track?.uri === track?.uri) && 25 | !spotifyPlayer?.paused 26 | ) { 27 | setisPlaying(true); 28 | } else { 29 | setisPlaying(false); 30 | } 31 | } 32 | }, [spotifyPlayer, track]); 33 | 34 | return ( 35 |
    40 |
    41 |
    42 | 43 | {track ? ( 44 | <> 45 | 46 | {track?.index < 10 ? `0${track?.index}` : track?.index} 47 | 48 | 49 | 55 | 56 | 57 | ) : ( 58 | 59 | )} 60 | 61 | 62 | {image ? ( 63 |
    64 | {track?.track?.album?.images || track?.album?.images ? ( 65 | el.url).url || 71 | track?.track?.album?.images 72 | ?.slice(0) 73 | .reverse() 74 | .find((el) => el.url).url 75 | } 76 | layout="fixed" 77 | width={40} 78 | height={40} 79 | priority 80 | alt={track?.track?.name || track?.name} 81 | /> 82 | ) : null} 83 |
    84 | ) : null} 85 |
    86 | 87 | {track ? ( 88 | track?.track?.name || track?.name 89 | ) : ( 90 | 91 | )} 92 | 93 | {track ? ( 94 | 95 | {link 96 | ? track?.track?.album?.artists?.map((artist, i) => ( 97 | 98 | 99 | {artist?.name} 100 | 101 | 102 | )) 103 | : null} 104 | {link 105 | ? track?.artists?.map((artist, i) => ( 106 | 107 | 108 | {artist?.name} 109 | 110 | 111 | )) 112 | : null} 113 | 114 | ) : ( 115 | 116 | )} 117 |
    118 |
    119 |
    120 | 131 |
    132 | 133 | {track?.track ? ( 134 | moment(track?.added_at).fromNow() 135 | ) : type === "playlist" ? ( 136 | 137 | ) : null} 138 | 139 |
    140 |
    141 | 142 | {track ? ( 143 | <> 144 | {/* J */} 145 | 146 | {!track?.track 147 | ? moment(track?.duration_ms).format("m:ss") 148 | : moment(track?.track?.duration_ms).format("m:ss")} 149 | 150 | 151 | ) : ( 152 | 153 | 156 | 157 | 158 | )} 159 | 160 |
    161 |
    162 |
    163 | ); 164 | }); 165 | -------------------------------------------------------------------------------- /components/AudioPlayer/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import styles from "./.module.sass"; 3 | import { connect } from "react-redux"; 4 | import { AiOutlineHeart } from "react-icons/ai"; 5 | import "react-input-range/lib/css/index.css"; 6 | import RangeInput from "../RangeInput"; 7 | import NextImage from "../NextImage"; 8 | import moment from "moment"; 9 | import Head from "next/head"; 10 | import { 11 | setDeviceId, 12 | setSpotifyPlayer, 13 | getPlayerState, 14 | playPauseTrack, 15 | } from "../../redux/actions"; 16 | import { 17 | IoIosSkipForward, 18 | IoIosSkipBackward, 19 | IoIosShuffle, 20 | } from "react-icons/io"; 21 | import { TiArrowLoop } from "react-icons/ti"; 22 | import { BiPlay, BiPause } from "react-icons/bi"; 23 | import { MdQueueMusic } from "react-icons/md"; 24 | import { VscUnmute, VscMute } from "react-icons/vsc"; 25 | 26 | const mapStateToProps = (state) => state; 27 | const mapDispatchToProps = { 28 | getPlayerState, 29 | setDeviceId, 30 | setSpotifyPlayer, 31 | playPauseTrack, 32 | }; 33 | 34 | export const AudioPlayer = ({ 35 | user, 36 | token, 37 | setDeviceId, 38 | setSpotifyPlayer, 39 | playPauseTrack, 40 | }) => { 41 | const [player, setPlayer] = useState(null); 42 | const [playerState, setPlayerState] = useState({}); 43 | const [seconds, setSeconds] = useState(0); 44 | const [volume, setVolume] = useState(100); 45 | const [mute, setMute] = useState(false); 46 | 47 | //************************************************************// 48 | // INITIALIZE SPOTIFY SDK PLAYER 49 | //************************************************************// 50 | useEffect(() => { 51 | if (user?.product === "premium" && !player) { 52 | const script = document.createElement("script"); 53 | script.src = "https://sdk.scdn.co/spotify-player.js"; 54 | script.async = true; 55 | document.body.appendChild(script); 56 | 57 | window.onSpotifyWebPlaybackSDKReady = () => { 58 | const player = new window.Spotify.Player({ 59 | name: "NEXTJS SPOTIFY CLONE", 60 | getOAuthToken: (cb) => { 61 | cb(token); 62 | }, 63 | volume: 1, 64 | }); 65 | 66 | setPlayer(player); 67 | 68 | player?.addListener("ready", ({ device_id }) => { 69 | setDeviceId(device_id); 70 | if (device_id) playPauseTrack(device_id, token); 71 | player?.getCurrentState().then((res) => { 72 | res ? setPlayerState(res) : setPlayerState(playerState); 73 | res?.context ? setSpotifyPlayer({ ...res, player }) : null; 74 | }); 75 | player?.addListener("player_state_changed", (state) => { 76 | if (state) { 77 | player?.getCurrentState().then((res) => { 78 | res ? setPlayerState(res) : setPlayerState(playerState); 79 | res?.context ? setSpotifyPlayer({ ...res, player }) : null; 80 | }); 81 | } 82 | }); 83 | }); 84 | player.connect(); 85 | }; 86 | } 87 | 88 | return () => { 89 | // player?.pause(); 90 | }; 91 | }, []); 92 | 93 | useEffect(() => { 94 | let timeout; 95 | if ( 96 | seconds < playerState?.track_window?.current_track?.duration_ms && 97 | !playerState?.paused 98 | ) { 99 | timeout = setTimeout(() => { 100 | setSeconds(seconds + 1000); 101 | }, 1000); 102 | } 103 | if (playerState.paused) clearTimeout(timeout); 104 | return () => clearTimeout(timeout); 105 | }, [seconds, playerState]); 106 | 107 | useEffect(() => { 108 | if (playerState?.position) setSeconds(playerState?.position); 109 | }, [playerState]); 110 | 111 | useEffect(() => { 112 | if (player?._options?.volume) setVolume(player?._options?.volume * 100); 113 | }, [player]); 114 | 115 | useEffect(() => { 116 | volume && !mute ? player?.setVolume(volume / 100) : player?.setVolume(0); 117 | }, [volume, mute]); 118 | 119 | return ( 120 | <> 121 | {playerState && !playerState?.paused ? ( 122 | 123 | 124 | Spotify | {playerState?.track_window?.current_track?.name} •{" "} 125 | {playerState?.track_window?.current_track?.artists[0]?.name} 126 | 127 | 128 | ) : null} 129 | 130 |
    131 |
    132 |
    133 |
    134 | {playerState?.track_window?.current_track?.album?.images[1] 135 | .url ? ( 136 | 147 | ) : null} 148 |
    149 |
    150 |

    151 | {playerState?.track_window?.current_track?.name} 152 |

    153 | 154 | {playerState?.track_window?.current_track?.artists[0]?.name} 155 | 156 |
    157 |
    158 | 161 |
    162 |
    163 |
    164 |
    165 |
    166 |
    167 | 178 | 185 | 193 | 196 | 199 |
    200 |
    201 | 202 | {moment(seconds).format("m:ss")} 203 | 204 |
    205 | { 212 | player.seek(value); 213 | }} 214 | /> 215 |
    216 | 217 | {moment( 218 | playerState?.track_window?.current_track?.duration_ms 219 | ).format("m:ss")} 220 | 221 |
    222 |
    223 |
    224 | 225 |
    226 |
    227 | 230 | 237 |
    238 |
    239 | {}} 244 | onChange={(value) => { 245 | setVolume(value); 246 | }} 247 | /> 248 |
    249 | {volume}% 250 |
    251 |
    252 |
    253 |
    254 | 255 | ); 256 | }; 257 | 258 | export default connect(mapStateToProps, mapDispatchToProps)(AudioPlayer); 259 | -------------------------------------------------------------------------------- /pages/[work]/[id]/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useState, useEffect } from "react"; 2 | import Head from "next/head"; 3 | import styles from "./.module.sass"; 4 | import NextImage from "../../../components/NextImage"; 5 | import { container } from "../../../components/AppMain/.module.sass"; 6 | import { connect } from "react-redux"; 7 | import { getWorkDetails, clearReducer } from "../../../redux/actions"; 8 | import { useRouter } from "next/router"; 9 | import VerifiedIcon from "../../../public/assets/icons/verified"; 10 | import Link from "next/link"; 11 | import Error from "next/error"; 12 | import numerize from "../../../utils/numerize"; 13 | import PlayPauseBtn from "../../../components/PlayPauseBtn"; 14 | import TracksTable from "../../../components/TracksTable"; 15 | import PlaylistRow from "../../../components/PlaylistsRow"; 16 | 17 | const mapStateToProps = (state) => state; 18 | const mapDispatchToProps = { getWorkDetails, clearReducer }; 19 | 20 | export default connect( 21 | mapStateToProps, 22 | mapDispatchToProps 23 | )(function WorkDetailsPage({ 24 | token, 25 | countryCode, 26 | workView, 27 | getWorkDetails, 28 | clearReducer, 29 | spotifyPlayer, 30 | }) { 31 | const router = useRouter(); 32 | const { work, id } = router.query; 33 | const [isFound, setisFound] = useState(false); 34 | const [isPlaying, setisPlaying] = useState(false); 35 | 36 | useEffect(() => { 37 | if (spotifyPlayer) { 38 | if ( 39 | spotifyPlayer?.context?.uri === workView?.uri && 40 | !spotifyPlayer?.paused 41 | ) { 42 | setisPlaying(true); 43 | } else { 44 | setisPlaying(false); 45 | } 46 | } 47 | }, [spotifyPlayer, workView]); 48 | 49 | useLayoutEffect(() => { 50 | if (["playlist", "artist", "album"].includes(work) && id) { 51 | setisFound(true); 52 | getWorkDetails(token, work, id, countryCode); 53 | } 54 | return () => { 55 | clearReducer({}); 56 | }; 57 | }, [id]); 58 | 59 | if (!isFound && !workView) return ; 60 | return ( 61 | <> 62 | 63 | Spotify App | {workView?.name || "Loading"} 64 | 65 |
    75 |
    76 |
    81 |
    84 |
    89 | {workView?.images?.length ? ( 90 | 98 | ) : null} 99 |
    100 |
    101 |
    104 |
    105 | {workView?.type ? ( 106 | 107 | {workView?.type === "artist" ? ( 108 | <> 109 | Verified Artist 110 | 111 | ) : ( 112 | workView?.type 113 | )} 114 | 115 | ) : ( 116 | 117 | )} 118 | 119 | {workView?.name ? ( 120 |

    {workView?.name}

    121 | ) : ( 122 | 123 | )} 124 | 125 | {workView?.album_type || 126 | workView?.description || 127 | workView?.genres?.length ? ( 128 | <> 129 |

    136 |

    137 | {workView?.type === "playlist" || 138 | workView?.type === "album" ? ( 139 | workView?.type === "album" ? ( 140 | <> 141 | 142 | {workView?.artists[0]?.name} 143 | 144 | {`. ${ 145 | workView?.release_date?.split("-")[0] 146 | } . ${workView?.tracks?.items?.length} Tracks . ${ 147 | workView?.album_type 148 | }`} 149 | 150 | ) : ( 151 | <> 152 | {workView?.owner?.display_name} 153 | {`. ${numerize( 154 | workView?.followers?.total 155 | )} Followers . ${ 156 | workView?.tracks?.items?.length 157 | } Tracks`} 158 | 159 | ) 160 | ) : ( 161 | 162 | {numerize(workView?.followers?.total)} Followers 163 | 164 | )} 165 |

    166 | 167 | ) : workView?.type !== "artist" ? ( 168 | <> 169 | <> 170 | 171 | 172 | 173 | 174 | 175 | ) : null} 176 |
    177 |
    178 |
    179 |
    180 | 181 | 187 | 188 |
    189 |
    190 | {work === "playlist" || work === "album" ? ( 191 | 198 | ) : null} 199 | {workView?.artists?.length && work === "album" ? ( 200 |
    201 |
    202 | 209 |
    210 |
    211 | ) : null} 212 | {work === "artist" ? ( 213 | <> 214 | { 220 | return track.album.uri; 221 | })} 222 | /> 223 |
    224 |
    225 | 232 | 239 |
    240 |
    241 | 242 | ) : null} 243 |
    244 | 245 | ); 246 | }); 247 | export async function getStaticProps(context) { 248 | return { 249 | props: {}, 250 | notFound: 251 | !["playlist", "artist", "album"].includes(context.params.work) || 252 | !context.params.id, 253 | }; 254 | } 255 | export async function getStaticPaths() { 256 | return { 257 | paths: [], 258 | fallback: true, 259 | }; 260 | } 261 | -------------------------------------------------------------------------------- /public/assets/icons/icon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function () { 4 | return ( 5 | 12 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /public/assets/icons/logo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function ({ width, height }) { 4 | return ( 5 | 12 | 19 | 20 | ); 21 | } 22 | --------------------------------------------------------------------------------