├── .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 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/public/assets/icons/arrowRight.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function () {
4 | return (
5 |
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 |
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 |
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 |
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 |
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 |
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 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/public/assets/icons/plusIcon.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function () {
4 | return (
5 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
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 |
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 |
189 |
190 | {work === "playlist" || work === "album" ? (
191 |
198 | ) : null}
199 | {workView?.artists?.length && work === "album" ? (
200 |
211 | ) : null}
212 | {work === "artist" ? (
213 | <>
214 |
{
220 | return track.album.uri;
221 | })}
222 | />
223 |
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 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/public/assets/icons/logo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function ({ width, height }) {
4 | return (
5 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------