Initial commit of spotify_api application

This commit is contained in:
Juthatip McDevitt 2024-03-08 16:06:01 -06:00
commit 9a726805fc
33 changed files with 14389 additions and 0 deletions

24
spotify_api/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

70
spotify_api/README.md Normal file
View file

@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

13096
spotify_api/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

41
spotify_api/package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "spotify_api",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"react-scripts": "5.0.1",
"styled-components": "^6.1.8",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="./spotify.png" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app"/>
<title>Spotify API | By JTPMcDevitt</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

28
spotify_api/src/App.js Normal file
View file

@ -0,0 +1,28 @@
import React, { useEffect } from 'react'
import Login from './components/login/Login'
import {useStateProvider} from './util/StateProvider'
import { reducerCases } from './util/Constants'
import Home from './components/home/Home'
const App = () => {
const [{token}, dispatch] = useStateProvider()
useEffect(() => {
const hash = window.location.hash;
if(hash){
const token = hash.substring(1).split("&")[0].split("=")[1];
dispatch({type:reducerCases.SET_TOKEN, token})
}
}, [token, dispatch]);
return (
<div>
{
token? <Home/> : <Login/>
}
</div>
)
}
export default App

View file

@ -0,0 +1,48 @@
import React, { useEffect } from 'react'
import './home.css'
import Sidebar from '../sidebar/Sidebar'
import Navbar from '../navbar/Navbar'
import Main from '../main/Main'
import MusicPlayer from '../musicPlayer/MusicPlayer'
import { useStateProvider } from '../../util/StateProvider'
import axios from 'axios'
import { reducerCases } from '../../util/Constants'
const Spotify = () => {
const [{token}, dispatch] = useStateProvider();
useEffect(() => {
const getUserInfo = async () => {
const {data} = await axios.get('https://api.spotify.com/v1/me', {
headers:{
Authorization:"Bearer "+token,
"Content-Type": "application/json"
},
});
const userInfo = {
userId: data.id,
userName: data.display_name,
};
dispatch({type: reducerCases.SET_USER, userInfo})
};
getUserInfo();
}, [dispatch, token])
return (
<>
<div className="Home">
<div className="home-sidebar"><Sidebar/></div>
<div className="home-nav">
<Navbar/>
</div>
<div className="home-main">
<Main/>
</div>
<div className="home-musicPlayer"><MusicPlayer/></div>
</div>
</>
)
}
export default Spotify

View file

@ -0,0 +1,50 @@
.Home{
max-width: 100vw;
max-height: 100vh;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(10, 1fr);
grid-column-gap: 0px;
grid-row-gap: 0px;
}
.home-sidebar{
grid-area: 1 / 1 / 11 / 2;
width: 420px;
height: 100vh;
background-color: black;
}
.home-nav{
grid-area: 1 / 2 / 2 / 6;
background-color: black;
width: 100%;
height: 100%;
}
.home-main{
grid-area: 2/ 2 / 10 / 6;
background-color: black;
overflow: auto;
width: 100%;
height: 100%;
}
.home-musicPlayer{
grid-area: 10 / 1 / 11 / 6;
width: 100%;
height: 100%;
background-color: black;
}
/*===== Responsive =====*/
@media screen and (max-width: 1200px){
.home-sidebar{
width: 350px;
}
}
@media screen and (max-width: 1000px){
.home-sidebar{
width: 350px;
}
}
@media screen and (max-width: 850px){
.home-sidebar{
width: 300px;
}
}

View file

@ -0,0 +1,32 @@
import React from 'react'
import './login.css'
import Img from '../../images/spotify_login.png'
const Login = () => {
const handleClick = () => {
const ClientID = process.env.REACT_APP_CLIENT_ID;
const Redirect_Url = process.env.REACT_APP_REDIRECT_URL;
const API_URL = process.env.REACT_APP_API_URL;
const scope = [
'user-read-email',
'user-read-private',
'user-modify-playback-state',
'user-read-playback-state',
'user-read-currently-playing',
'user-read-playback-position',
'user-top-read',
'user-read-recently-played',
];
window.location.href = `${API_URL}?client_id=${ClientID}&redirect_uri=${Redirect_Url}&scope=${scope.join(" ")}&response_type=token&show_daialog=true`;
};
return (
<div className='login'>
<img src={Img} alt="" />
<button onClick={handleClick}>Connect Spotify</button>
</div>
)
}
export default Login

View file

@ -0,0 +1,29 @@
.login{
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #121212;
gap: 100px;
}
.login img{
height: 20vh;
}
.login button{
padding: 15px 30px;
border-radius: 30px;
border: none;
font-size: 20px;
font-weight: 600;
letter-spacing: 1px;
text-transform: capitalize;
cursor: pointer;
background-color: #1db954;
color: #121212;
transition: all 0.5s ease;
}
.login button:hover{
background-color: #77ce95;
}

View file

@ -0,0 +1,147 @@
import React, { useEffect } from 'react'
import './main.css'
import { AiFillClockCircle } from "react-icons/ai";
import { useStateProvider } from '../../util/StateProvider';
import axios from 'axios';
import { reducerCases } from '../../util/Constants';
const Main = () => {
const [{token, selectedPlaylistId, selectedPlaylist}, dispatch] = useStateProvider();
useEffect(() => {
const getInitialPlaylist = async () => {
const response = await axios.get(`https://api.spotify.com/v1/playlists/${selectedPlaylistId}`,
{
headers:{
Authorization:"Bearer "+token,
"Content-Type": "application/json"
},
}
);
const selectedPlaylist = {
id: response.data.id,
name: response.data.name,
description: response.data.description.startsWith("<a") ? "" : response.data.description,
image: response.data.images[0].url,
tracks: response.data.tracks.items.map(({track}) => ({
id: track.id,
name: track.name,
artists: track.artists.map((artist) => artist.name),
image: track.album.images[2].url,
duration: track.duration_ms,
album: track.album.name,
context_uri: track.album.uri,
track_number: track.track_number,
})),
}
dispatch({type: reducerCases.SET_PLAYLISTS, selectedPlaylist})
};
getInitialPlaylist();
}, [token, dispatch, selectedPlaylistId])
const minAndSec = (ms) => {
const minutes = Math.floor(ms/60000);
const seconds = ((ms % 60000)/1000).toFixed(0);
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
}
const playTrack = async (id, name, artists, image, context_uri, track_number) => {
//PREMIUM_REQUIRED
const response = await axios.put('https://api.spotify.com/v1/me/player/play',
{
context_uri,
offset:{
position: track_number - 1
},
position_ms: 0,
},
{
headers: {
Authorization:"Bearer "+token,
"Content-Type": "application/json"
}
}
);
if(response.status === 204){
const currentlyPlaying = {id, name, artists, image}
dispatch({type: reducerCases.SET_PLAYING, currentlyPlaying})
dispatch({type: reducerCases.SET_PLAYER_STATE, playerState: true})
}else dispatch({type: reducerCases.SET_PLAYER_STATE, playerState: true})
}
return (
<div className='Main'>
{
selectedPlaylist && (
<>
<div className="main-playlist">
<div className="main-image">
<img src={selectedPlaylist.image} alt="" />
</div>
<div className="main-detail">
<span className="main-type">Playlist</span>
<h1 className='main-title'>{selectedPlaylist.name}</h1>
<p className="main-desc">{selectedPlaylist.description}</p>
</div>
</div>
<div className="main-list">
<div className="main-header">
<div className="header-col">
<span>#</span>
</div>
<div className="header-col">
<span>Title</span>
</div>
<div className="header-col album">
<span>Album</span>
</div>
<div className="header-col">
<span><AiFillClockCircle/></span>
</div>
</div>
<div className="main-tracks">
{selectedPlaylist.tracks.map(({
id,
name,
artists,
image,
duration,
album,
context_uri,
track_number,
}, index) => {
return(
<div className="main-row" key={id} onClick={() => playTrack(id, name, artists, image, context_uri, track_number)}>
<div className="main-col">
<span>{index+1}</span>
</div>
<div className="main-col detail">
<div className="image">
<img src={image} alt="track" />
</div>
<div className="main-info">
<span className="main-name">{name}</span>
<span>{artists.join(", ")}</span>
</div>
</div>
<div className="main-col album">
<span>{album}</span>
</div>
<div className="main-col">
<span>{minAndSec(duration)}</span>
</div>
</div>
)
}
)}
</div>
</div>
</>
)
}
</div>
)
}
export default Main

View file

@ -0,0 +1,110 @@
.Main{
color: white;
background-color: #1b1b1b;
margin-right: 10px;
}
.main-playlist{
margin: 0 20px;
display: flex;
align-items: center;
gap: 20px;
}
.main-image img{
margin-top: 20px;
height: 225px;
border-radius: 5px;
box-shadow: rgba(0, 0, 0, 0.3) 0px 25px 50px -15px;
}
.main-detail{
display: flex;
flex-direction: column;
color: #D8D9DA;
}
.main-title{
color: white;
font-size: 100px;
font-weight: 800;
}
.main-list .main-header{
display: grid;
grid-template-columns: 0.5fr 5fr 3fr 0.5fr;
margin: 50px;
padding: 10px;
position: sticky;
border-bottom: 1px solid rgba(211, 211, 211, 0.466);
}
.main-list .main-tracks{
margin: 0 50px;
display: flex;
flex-direction: column;
}
.main-row:first-child{
margin-top: -30px;
}
.main-row:last-child{
margin-bottom: 150px;
}
.main-tracks .main-row{
display: grid;
grid-template-columns: 0.5fr 5fr 3fr 0.5fr;
padding: 5px;
}
.main-tracks .main-row:hover{
background-color: #44444488;
border-radius: 5px;
}
.main-col{
display: flex;
align-items: center;
color: gray;
}
.main-row .image img{
width: 40px;
border-radius: 5px;
}
.main-row .detail{
display: flex;
gap: 10px;
}
.main-info{
display: flex;
flex-direction: column;
}
.main-info .main-name{
font-size: 16px;
color: white;
}
.main-info span{
margin-top: 5px;
font-size: 14px;
color: gray;
}
/*===== Responsive =====*/
@media screen and (max-width: 1200px){
.main-image img{
height: 200px;
}
.main-title{
font-size: 60px;
}
}
@media screen and (max-width: 850px) {
.main-image img{
height: 120px;
}
.main-title{
font-size: 40px;
}
.main-list .main-header{
grid-template-columns: 0.5fr 5fr 0.5fr;
}
.main-list .header-col.album{
display: none;
}
.main-tracks .main-row{
grid-template-columns: 0.5fr 5fr 0.5fr;
}
.main-col.album{
display: none;
}
}

View file

@ -0,0 +1,17 @@
import React from 'react'
import './musicPlayer.css'
import CurrentTrack from './currentTrack/CurrentTrack'
import { PlayerControl } from './playerControl/PlayerControl'
import Volume from './volume/Volume'
const Footer = () => {
return (
<div className='MusicPlayer'>
<CurrentTrack/>
<PlayerControl/>
<Volume/>
</div>
)
}
export default Footer

View file

@ -0,0 +1,56 @@
import React, { useEffect } from 'react'
import './currentTrack.css'
import { useStateProvider } from '../../../util/StateProvider'
import axios from 'axios';
import { reducerCases } from '../../../util/Constants';
import { CiHeart } from "react-icons/ci";
const CurrentTrack = () => {
const [{token, currentlyPlaying}, dispatch] = useStateProvider();
useEffect(() => {
const getcurrentTrack = async () =>{
const response = await axios.get('https://api.spotify.com/v1/me/player/currently-playing', {
headers: {
Authorization:"Bearer "+token,
"Content-Type": "application/json"
}
});
if(response.data !== ""){
const {item} = response.data;
const currentlyPlaying = {
id: item.id,
name: item.name,
artists: item.artists.map((artist) => artist.name),
image: item.album.images[2].url,
};
dispatch({type: reducerCases.SET_PLAYING, currentlyPlaying})
}
}
getcurrentTrack();
}, [token, dispatch, currentlyPlaying])
return (
<div className='CurrentTrack'>
{
currentlyPlaying && (
<div className="track">
<div className="track-image">
<img src={currentlyPlaying.image} alt="currentlyplaying" />
</div>
<div className="track-info">
<p>{currentlyPlaying.name}</p>
<span>{currentlyPlaying.artists.join(", ")}</span>
</div>
<div className="track-fav">
<CiHeart/>
</div>
</div>
)
}
</div>
)
}
export default CurrentTrack

View file

@ -0,0 +1,43 @@
.track{
display: flex;
align-items: center;
gap: 20px;
}
.track .track-info{
display: flex;
flex-direction: column;
gap: 2px;
}
.track-image img{
width: 60px;
border-radius: 5px;
}
.track-info p{
font-size: 16px;
}
.track-info span{
font-size: 12px;
color: gray;
}
.track-fav{
font-size: 22px;
color: lightgray;
}
/*===== Responsive =====*/
@media screen and (max-width: 850px){
.track{
gap: 10px;
}
.track-image img{
width: 50px;
}
.track-info p{
font-size: 14px;
}
.track-info span{
font-size: 10px;
}
.track-fav{
font-size: 16px;
}
}

View file

@ -0,0 +1,11 @@
.MusicPlayer{
background-color: black;
height: 100%;
width: 100%;
display: grid;
grid-template-columns: 1fr 2fr 1fr;
align-items: center;
justify-content: center;
color: white;
padding: 10px 20px 10px 20px;
}

View file

@ -0,0 +1,82 @@
import React from 'react'
import './playerControl.css'
import { FaPlayCircle } from "react-icons/fa";
import { FaPauseCircle } from "react-icons/fa";
import { FaShuffle } from "react-icons/fa6";
import { FaRepeat } from "react-icons/fa6";
import { IoPlaySkipForwardSharp } from "react-icons/io5"
import { IoPlaySkipBackSharp } from "react-icons/io5";
import { useStateProvider } from '../../../util/StateProvider';
import axios from 'axios';
import { reducerCases } from '../../../util/Constants';
export const PlayerControl = () => {
const [{token, playerState}, dispatch] = useStateProvider();
const changeTrack = async (type) => {
await axios.post(`https://api.spotify.com/v1/me/player/${type}`,{},
//PREMIUM_REQUIRED
{
headers: {
Authorization:"Bearer "+token,
"Content-Type": "application/json"
}
});
const response = await axios.get('https://api.spotify.com/v1/me/player/currently-playing', {
headers: {
Authorization:"Bearer "+token,
"Content-Type": "application/json"
}
});
if(response.data !== ""){
const {item} = response.data;
const currentlyPlaying = {
id: item.id,
name: item.name,
artists: item.artists.map((artist) => artist.name),
image: item.album.images[2].url,
};
dispatch({type: reducerCases.SET_PLAYING, currentlyPlaying})
}else
dispatch({type: reducerCases.SET_PLAYING, currentlyPlaying: null})
}
const changeState = async () =>{
const state = playerState ? "pause" : "play";
await axios.put(`https://api.spotify.com/v1/me/player/${state}`,{},
{
headers: {
Authorization:"Bearer "+token,
"Content-Type": "application/json"
}
});
dispatch({type: reducerCases.SET_PLAYER_STATE, playerState: !playerState})
}
return (
<>
<div className='PlayerControl'>
<div className="playerControl-control">
<div className="player-shuffle">
<FaShuffle/>
</div>
<div className="player-prev">
<IoPlaySkipBackSharp onClick={() => changeTrack("previous")}/>
</div>
<div className="player-state">
{playerState ? <FaPauseCircle onClick={changeState}/> : <FaPlayCircle onClick={changeState}/>}
</div>
<div className="player-next">
<IoPlaySkipForwardSharp onClick={() => changeTrack("next")}/>
</div>
<div className="play-repeat">
<FaRepeat/>
</div>
</div>
<div className="PlayerControl-play">
<input type="range" value="0" id="progress"/>
</div>
</div>
</>
)
}

View file

@ -0,0 +1,56 @@
.PlayerControl{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.playerControl-control{
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
}
.playerControl-control svg{
color: gray;
font-size: 18px;
transition: all 0.3s ease;
cursor: pointer;
}
.playerControl-control svg:hover{
color: white;
}
.playerControl-control .player-state svg{
color: white;
font-size: 36px;
}
.PlayerControl-play{
display: flex;
justify-content: center;
align-items: center;
}
.PlayerControl-play #progress{
width: 500px;
height: 3px;
border-radius: 5px;
background-color: gray;
margin: 5px;
cursor: pointer;
}
.PlayerControl-play input[type="range"]::-moz-range-thumb{
border: none;
background-color: transparent;
}
/*===== Responsive =====*/
@media screen and (max-width: 1200px){
.PlayerControl-play #progress{
width: 350px;
}
}
@media screen and (max-width: 850px){
.PlayerControl-play #progress{
width: 250px;
}
.playerControl-control{
gap: 10px;
}
}

View file

@ -0,0 +1,42 @@
import React from 'react'
import './volume.css'
import { useStateProvider } from '../../../util/StateProvider'
import { MdOpenInFull } from "react-icons/md";
import { IoVolumeLow } from "react-icons/io5";
import { MdImportantDevices } from "react-icons/md";
import { HiOutlineQueueList } from "react-icons/hi2";
import { TbMicrophone2 } from "react-icons/tb";
import { AiOutlinePlaySquare } from "react-icons/ai";
import axios from 'axios';
const Volume = () => {
const [{token}] = useStateProvider();
const setVolume = async (e) => {
await axios.put('https://api.spotify.com/v1/me/player/volume',{},
//PREMIUM_REQUIRED
{
params:{
volume_percent: parseInt(e.target.value)
},
headers:{
Authorization:"Bearer "+token,
"Content-Type": "application/json"
}
});
};
return (
<div className='Volume'>
<AiOutlinePlaySquare/>
<TbMicrophone2/>
<HiOutlineQueueList/>
<MdImportantDevices/>
<div className="volume-icon">
<IoVolumeLow/>
<input type="range" min={0} max={100} onMouseUp= {(e) => setVolume(e)}/>
</div>
<MdOpenInFull/>
</div>
)
}
export default Volume

View file

@ -0,0 +1,28 @@
.Volume{
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
color: gray;
cursor: pointer;
font-size: 20px;
}
.Volume svg:hover{
color: white;
}
.volume-icon{
display: flex;
align-items: center;
gap: 10px;
}
.volume-icon input[type="range"]{
width: 100px;
height: 3px;
border-radius: 5px;
background-color: gray;
cursor: pointer;
}
.volume-icon input[type="range"]::-moz-range-thumb{
border: none;
background-color: transparent;
}

View file

@ -0,0 +1,30 @@
import React from 'react'
import './navbar.css'
import { IoSearch } from "react-icons/io5";
import { CgProfile } from "react-icons/cg";
import { useStateProvider } from '../../util/StateProvider';
const Navbar = () => {
const [{userInfo}] = useStateProvider();
return (
<div className='Navbar'>
<div className="nav-search">
<div className="navbar-search">
<IoSearch/>
<input type="text" placeholder='What do you want to play?' />
</div>
</div>
<div className="userProfile">
<a href="/">
<CgProfile/>
<span>{userInfo?.userName}</span>
</a>
</div>
</div>
)
}
export default Navbar

View file

@ -0,0 +1,62 @@
.Navbar{
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background-color: #1b1b1b;
margin-top: 10px;
margin-right: 10px;
border-radius: 5px 5px 0px 0px;
position: sticky;
}
.nav-search{
display: flex;
align-items: center;
background-color: #313233;
width: 25%;
padding: 15px 25px;
border-radius: 30px;
border: 1px solid white;
margin-left: 20px;
}
.navbar-search{
display: flex;
gap: 10px;
color: #D8D9DA;
font-size: 22px;
}
.navbar-search input{
border: none;
width: 100%;
height: 20px;
outline: none;
background-color: #313233;
color: #D8D9DA;
}
.userProfile{
display: flex;
justify-content: center;
align-items: center;
color: #D8D9DA;
}
.userProfile a{
display: flex;
gap: 10px;
font-size: 22px;
color: #1db954;
margin-right: 20px;
}
.userProfile span{
font-size: 16px;
}
/*===== Responsive =====*/
@media screen and (max-width: 1500px){
.nav-search{
width: 40%;
}
}
@media screen and (max-width: 1200px){
.nav-search{
width: 50%;
}
}

View file

@ -0,0 +1,50 @@
import React, { useEffect } from 'react'
import './playList.css'
import {useStateProvider} from '../../util/StateProvider'
import axios from 'axios'
import { reducerCases } from '../../util/Constants'
const PlayList = () => {
const [{token, playlists}, dispatch] = useStateProvider();
useEffect(() => {
const getPlaylist = async () =>{
const response = await axios.get('https://api.spotify.com/v1/me/playlists', {
headers: {
Authorization:"Bearer "+token,
"Content-Type": "application/json"
}
});
const {items} = response.data
const playlists = items.map(({name, id, images}) =>{
return {name, id, images}
});
dispatch({type: reducerCases.SET_PLAYLIST, playlists})
}
getPlaylist();
}, [token, dispatch])
const changeCurrentPlaylist = (selectedPlaylistId) =>{
dispatch({type: reducerCases.SET_PLAYLIST_ID,selectedPlaylistId});
}
return (
<div className='Playlist'>
<ul>
{
playlists.map(({name, id, images}) => {
console.log(playlists)
return(
<div className='playlist-side'>
<img src={images[0].url} alt="" />
<li key={id} onClick={() => changeCurrentPlaylist(id)}>{name}</li>
</div>
)
})
}
</ul>
</div>
)
}
export default PlayList

View file

@ -0,0 +1,30 @@
.Playlist{
height: 100%;
overflow: hidden;
}
.Playlist ul{
display: flex;
height: 70vh;
max-height: 100%;
overflow: auto;
}
.playlist-side{
display: flex;
padding: 10px;
}
.playlist-side:hover{
padding: 10px;
background-color: #44444488;
font-size: 16px;
border-radius: 5px;
}
.Playlist img{
width: 50px;
}
.playlist-side li{
align-items: center;
font-size: 16px;
margin-left: 10px;
}

View file

@ -0,0 +1,27 @@
import React from 'react'
import './sidebar.css'
import { GrHomeRounded } from "react-icons/gr";
import { IoSearch } from "react-icons/io5";
import { BiLibrary } from "react-icons/bi";
import PlayList from '../playlist/PlayList';
const Sidebar = () => {
return (
<div className='Sidebar'>
<div className="sidebar-top">
<ul>
<li><GrHomeRounded/><span>Home</span></li>
<li><IoSearch/><span>Search</span></li>
</ul>
</div>
<div className="sidebar-bottom">
<ul>
<li><BiLibrary/><span>Your Library</span></li>
</ul>
<PlayList/>
</div>
</div>
)
}
export default Sidebar

View file

@ -0,0 +1,52 @@
.Sidebar{
background-color: black;
color: white;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.sidebar-top, .sidebar-bottom{
display: flex;
flex-direction: column;
background-color: #1b1b1b;
margin: 10px;
border-radius: 5px;
}
.sidebar-bottom{
display: flex;
flex-direction: column;
background-color: #1b1b1b;
margin-top: 0;
border-radius: 5px 5px 0px 0px;
}
.sidebar-top ul{
display: flex;
flex-direction: column;
padding: 25px;
gap: 25px;
}
.sidebar-bottom ul{
display: flex;
flex-direction: column;
padding-top: 25px;
padding-left: 25px;
padding-right: 25px;
padding-bottom: 10px;
gap: 10px;
}
.sidebar-top li, .sidebar-bottom li{
display: flex;
gap: 15px;
font-size: 20px;
font-weight: 600;
transition: all 0.3s ease;
color: #D8D9DA;
cursor: pointer;
}
.sidebar-top li:hover, .sidebar-bottom li:hover{
color: white;
}
.sidebar-top span, .sidebar-bottom span{
font-size: 16px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

14
spotify_api/src/index.css Normal file
View file

@ -0,0 +1,14 @@
@import url('https://fonts.googleapis.com/css2?family=Karla:ital,wght@0,200..800;1,200..800&display=swap');
*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Karla", sans-serif;
}
a{
text-decoration: none;
color: inherit;
}
ul{
list-style: none;
}

15
spotify_api/src/index.js Normal file
View file

@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {StateProvider} from './util/StateProvider'
import reducer, { initialState } from './util/reducer';
ReactDOM.render(
<React.StrictMode>
<StateProvider initialState = {initialState} reducer = {reducer}>
<App />
</StateProvider>
</React.StrictMode>,
document.getElementById('root')
);

View file

@ -0,0 +1,9 @@
export const reducerCases = {
SET_TOKEN: "SET_TOKEN",
SET_PLAYLIST: "SET_PLAYLIST",
SET_USER: "SET_USER",
SET_PLAYLISTS: "SET_PLAYLISTS",
SET_PLAYING: "SET_PLATING",
SET_PLAYER_STATE: "SET_PLAYER_STATE",
SET_PLAYLIST_ID: "SET_PLAYLIST_ID",
};

View file

@ -0,0 +1,12 @@
import { createContext, useContext, useReducer } from "react";
export const StateContext = createContext();
export const StateProvider = ({children, initialState, reducer}) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
export const useStateProvider = () => useContext(StateContext);

View file

@ -0,0 +1,62 @@
import { reducerCases } from "./Constants";
export const initialState = {
token:null,
playlists: [],
userInfo: null,
selectedPlaylistId: "0IcoOEhudoG27tCVJWX0DL",
selectedPlaylist: null,
currentlyPlaying: null,
playerState: false,
};
const reducer = (state, action) => {
switch(action.type) {
case reducerCases.SET_TOKEN:{
return {
...state,
token: action.token,
}
}
case reducerCases.SET_PLAYLIST:{
return{
...state,
playlists: action.playlists,
}
}
case reducerCases.SET_USER:{
return{
...state,
userInfo: action.userInfo,
}
}
case reducerCases.SET_PLAYLISTS:{
return{
...state,
selectedPlaylist: action.selectedPlaylist,
}
}
case reducerCases.SET_PLAYING:{
return{
...state,
currentlyPlaying: action.currentlyPlaying,
}
}
case reducerCases.SET_PLAYER_STATE:{
return{
...state,
playerState: action.playerState,
}
}
case reducerCases.SET_PLAYLIST_ID:{
return{
...state,
selectedPlaylistId: action.selectedPlaylistId,
}
}
default:
return state;
}
};
export default reducer;