├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── image │ ├── createPlaylist.PNG │ └── popularSong.PNG ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── actions └── index.js ├── component ├── buttons │ ├── icon-button.js │ ├── icon-button.module.css │ ├── next-page-button.js │ ├── play-button.js │ ├── play-button.module.css │ └── prev-page-button.js ├── cards │ ├── playlist-card-m.js │ ├── playlist-card-m.module.css │ ├── playlist-card-s.js │ ├── playlist-card-s.module.css │ ├── searchpage-card.js │ └── searchpage-card.module.css ├── footer │ ├── audio.js │ ├── footer-left.js │ ├── footer-left.module.css │ ├── footer-right.js │ ├── footer-right.module.css │ ├── footer.js │ ├── footer.module.css │ ├── player │ │ ├── music-control-box.js │ │ ├── music-control-box.module.css │ │ ├── music-progress-bar.js │ │ └── music-progress-bar.module.css │ ├── range-slider.js │ └── range-slider.module.css ├── icons │ ├── Corner.js │ ├── Down.js │ ├── DownloadApp.js │ ├── Home.js │ ├── HomeActive.js │ ├── Library.js │ ├── LibraryActive.js │ ├── Like.js │ ├── LikeActive.js │ ├── Logo.js │ ├── Logo.module.css │ ├── Loop.js │ ├── Mix.js │ ├── More.js │ ├── Next.js │ ├── Nextpage.js │ ├── Pause.js │ ├── Play.js │ ├── Prev.js │ ├── Prevpage.js │ ├── Profile.js │ ├── Search.js │ ├── SearchActive.js │ ├── Sound.js │ ├── SoundClose.js │ ├── Time.js │ └── index.js ├── playlist │ ├── playlist-details.js │ ├── playlist-details.module.css │ ├── playlist-track.js │ └── playlist-track.module.css ├── sidebar │ ├── mobile-navigation.js │ ├── mobile-navigation.module.css │ ├── navigation.js │ ├── navigation.module.css │ ├── playlist-button.js │ ├── playlist-button.module.css │ ├── playlist.js │ ├── playlist.module.css │ ├── sidebar.js │ └── sidebar.module.css ├── text │ ├── text-bold-l.js │ ├── text-bold-l.module.css │ ├── text-bold-m.js │ ├── text-bold-m.module.css │ ├── text-regular-m.js │ ├── text-regular-m.module.css │ ├── title-l.js │ ├── title-l.module.css │ ├── title-m.js │ ├── title-m.module.css │ ├── title-s.js │ └── title-s.module.css └── topnav │ ├── library-tab-btn.js │ ├── library-tab-btn.module.css │ ├── search-box.js │ ├── search-box.module.css │ ├── topnav.js │ └── topnav.module.css ├── constants └── index.js ├── data └── index.js ├── functions └── convertTime.js ├── hooks ├── useMousePosition.js └── useWindowSize.js ├── icons ├── corner.svg ├── down.svg ├── download-app.svg ├── home-active.svg ├── home.svg ├── library-active.svg ├── library.svg ├── like-active.svg ├── like.svg ├── logo.svg ├── loop.svg ├── mix.svg ├── next.svg ├── nextpage.svg ├── play.svg ├── prev.svg ├── prevpage.svg ├── profile.svg ├── search-active.svg ├── search.svg └── sound.svg ├── image └── now-play.gif ├── index.js ├── pages ├── home.js ├── home.module.css ├── library.js ├── library.module.css ├── playlist.js ├── playlist.module.css ├── search.js └── search.module.css ├── reducers └── index.js └── style ├── App.module.css ├── index.css └── variables.css /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # firebase 12 | /.firebase 13 | /.firebaserc 14 | /firebase.json 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify Web Player Clone 2 | 3 | A front-end clone project of the Spotify Web Player. The project was created using React. This is my first big React.js project. 4 | 5 | ## Preview Link 6 | - [Spotify Web Player Clone](https://spotify-clone-oguz3.web.app/) 7 | 8 | ## Tech/Framework Used 9 | * React 10 | * CSS -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-web-player", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.9", 7 | "@testing-library/react": "^11.2.5", 8 | "@testing-library/user-event": "^12.8.0", 9 | "react": "^17.0.1", 10 | "react-dom": "^17.0.1", 11 | "react-redux": "^7.2.2", 12 | "react-router-dom": "^5.2.0", 13 | "react-scripts": "4.0.3", 14 | "redux": "^4.0.5", 15 | "web-vitals": "^1.1.0" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject", 22 | "icon": "svgr src/icons -d src/component/icons --icon" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "@svgr/cli": "^5.5.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oguz3/spotify-web-player/feee581f89b62e5daa6eff8ef5feb1829568af71/public/favicon.ico -------------------------------------------------------------------------------- /public/image/createPlaylist.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oguz3/spotify-web-player/feee581f89b62e5daa6eff8ef5feb1829568af71/public/image/createPlaylist.PNG -------------------------------------------------------------------------------- /public/image/popularSong.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oguz3/spotify-web-player/feee581f89b62e5daa6eff8ef5feb1829568af71/public/image/popularSong.PNG -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Spotify Web Player | Oguzhan Ulukaya 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oguz3/spotify-web-player/feee581f89b62e5daa6eff8ef5feb1829568af71/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oguz3/spotify-web-player/feee581f89b62e5daa6eff8ef5feb1829568af71/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { BrowserRouter as Router, 3 | Switch, 4 | Route 5 | } from "react-router-dom"; 6 | import useWindowSize from './hooks/useWindowSize'; 7 | import Sidebar from './component/sidebar/sidebar'; 8 | import MobileNavigation from './component/sidebar/mobile-navigation'; 9 | import Footer from './component/footer/footer'; 10 | import Home from './pages/home'; 11 | import Search from './pages/search'; 12 | import Library from './pages/library'; 13 | import PlaylistPage from './pages/playlist'; 14 | 15 | import CONST from './constants/index'; 16 | import { PLAYLIST } from './data/index'; 17 | import styles from './style/App.module.css'; 18 | 19 | function App() { 20 | const size = useWindowSize(); 21 | 22 | return ( 23 | 24 |
25 | {size.width > CONST.MOBILE_SIZE 26 | ? 27 | : 28 | } 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
45 |
46 | ); 47 | } 48 | 49 | export default App; -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export const PLAYPAUSE = "PLAYPAUSE"; 2 | export const CHANGETRACK = "CHANGETRACK"; 3 | 4 | export const changePlay = (isPlaying) => { 5 | return { type: PLAYPAUSE, payload: isPlaying }; 6 | }; 7 | 8 | export const changeTrack = (trackKey) => { 9 | return { type: CHANGETRACK, payload: trackKey }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/component/buttons/icon-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as Icons from '../icons'; 3 | 4 | import styles from './icon-button.module.css'; 5 | 6 | class IconButton extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | isActive: false, 11 | } 12 | } 13 | 14 | render() { 15 | return ( 16 | 22 | ); 23 | } 24 | 25 | } 26 | export default IconButton -------------------------------------------------------------------------------- /src/component/buttons/icon-button.module.css: -------------------------------------------------------------------------------- 1 | .iconButton{ 2 | border: 0; 3 | background: transparent; 4 | width: 32px; 5 | height: 32px; 6 | padding: 0; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | .iconButton svg{ 12 | font-size: 16px; 13 | fill: hsla(0,0%,100%,.7); 14 | color: hsla(0,0%,100%,.7); 15 | } 16 | .iconButton:hover svg{ 17 | fill: rgb(255, 255, 255); 18 | color: rgb(255, 255, 255); 19 | } -------------------------------------------------------------------------------- /src/component/buttons/next-page-button.js: -------------------------------------------------------------------------------- 1 | import { useHistory } from "react-router-dom"; 2 | import * as Icons from '../icons'; 3 | 4 | function NextPageBtn() { 5 | let history = useHistory(); 6 | 7 | return ( 8 | 13 | ); 14 | } 15 | 16 | export default NextPageBtn; -------------------------------------------------------------------------------- /src/component/buttons/play-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { changePlay } from '../../actions'; 4 | import * as Icons from '../icons'; 5 | import IconButton from '../buttons/icon-button'; 6 | 7 | import styles from './play-button.module.css' 8 | 9 | function PlayButton(props) { 10 | return ( 11 |
props.changePlay(!props.isPlaying)}> 12 | {props.isPlaying && props.isthisplay 13 | ? } activeicon={}/> 14 | : } activeicon={}/> 15 | } 16 |
17 | ); 18 | } 19 | 20 | const mapStateToProps = (state) => { 21 | return { 22 | isPlaying: state.isPlaying 23 | }; 24 | }; 25 | 26 | export default connect(mapStateToProps, { changePlay })(PlayButton); -------------------------------------------------------------------------------- /src/component/buttons/play-button.module.css: -------------------------------------------------------------------------------- 1 | .playBtn{ 2 | width: 32px; 3 | height: 32px; 4 | border-radius: 32px; 5 | color: #000; 6 | border: none; 7 | background-color: #fff; 8 | padding: 0; 9 | transition: .2s; 10 | } 11 | .playBtn:hover{ 12 | transform: scale(1.1); 13 | } 14 | .playBtn svg{ 15 | fill: #000 !important; 16 | } -------------------------------------------------------------------------------- /src/component/buttons/prev-page-button.js: -------------------------------------------------------------------------------- 1 | import { useHistory } from "react-router-dom"; 2 | import * as Icons from '../icons'; 3 | 4 | function PrevPageBtn() { 5 | let history = useHistory(); 6 | 7 | return ( 8 | 13 | ); 14 | } 15 | 16 | export default PrevPageBtn; -------------------------------------------------------------------------------- /src/component/cards/playlist-card-m.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { connect } from 'react-redux'; 3 | import { changeTrack } from '../../actions'; 4 | import { Link } from "react-router-dom"; 5 | import TextBoldL from "../text/text-bold-l"; 6 | import TextRegularM from '../text/text-regular-m'; 7 | import PlayButton from '../buttons/play-button'; 8 | 9 | import styles from "./playlist-card-m.module.css"; 10 | 11 | function PlaylistCardM(props) { 12 | const[isthisplay, setIsthisPlay] = useState(false) 13 | 14 | useEffect(() => { 15 | setIsthisPlay(parseInt(props.data.index) === props.trackData.trackKey[0]) 16 | }) 17 | 18 | return ( 19 |
20 | 21 |
22 |
23 | {props.data.title} 24 |
25 |
26 | {props.data.title} 27 | {props.data.artist} 28 |
29 |
30 | 31 |
props.changeTrack([parseInt(props.data.index), 0])} 33 | className={`${styles.IconBox} ${isthisplay&&props.isPlaying ? styles.ActiveIconBox : ''}`} 34 | > 35 | 36 |
37 |
38 | ); 39 | } 40 | 41 | const mapStateToProps = (state) => { 42 | return { 43 | trackData: state.trackData, 44 | isPlaying: state.isPlaying 45 | }; 46 | }; 47 | 48 | export default connect(mapStateToProps, { changeTrack })(PlaylistCardM); 49 | -------------------------------------------------------------------------------- /src/component/cards/playlist-card-m.module.css: -------------------------------------------------------------------------------- 1 | .PlaylistCardSBox{ 2 | position: relative; 3 | overflow: hidden; 4 | } 5 | .PlaylistCardS{ 6 | position: relative; 7 | padding: 16px; 8 | background: #181818; 9 | border-radius: 4px; 10 | -webkit-transition: background-color .3s ease; 11 | transition: background-color .3s ease; 12 | } 13 | .ImgBox{ 14 | position: relative; 15 | -webkit-box-shadow: 0 -4px 12px rgb(0 0 0 / 50%); 16 | box-shadow: 0 -4px 12px rgb(0 0 0 / 50%); 17 | } 18 | .ImgBox img{ 19 | height: 100%; 20 | width: 100%; 21 | border-radius: 2px; 22 | } 23 | .IconBox{ 24 | position: absolute; 25 | bottom: 94px; 26 | right: 24px; 27 | opacity: 0; 28 | -webkit-transition: all .3s ease; 29 | transition: all .3s ease; 30 | -webkit-box-shadow: 0 8px 8px rgb(0 0 0 / 30%); 31 | box-shadow: 0 8px 8px rgb(0 0 0 / 30%); 32 | z-index: 2; 33 | border-radius: 500px; 34 | } 35 | .IconBox div{ 36 | background-color: #1db954; 37 | height: 40px; 38 | width: 40px; 39 | } 40 | .IconBox button{ 41 | height: 40px; 42 | width: 40px; 43 | -webkit-transition: -webkit-transform 33ms cubic-bezier(.3,0,0,1); 44 | transition: -webkit-transform 33ms cubic-bezier(.3,0,0,1); 45 | transition: transform 33ms cubic-bezier(.3,0,0,1); 46 | transition: transform 33ms cubic-bezier(.3,0,0,1),-webkit-transform 33ms cubic-bezier(.3,0,0,1); 47 | } 48 | .ActiveIconBox{ 49 | bottom: 106px; 50 | opacity: 1; 51 | } 52 | .IconBox button svg{ 53 | fill: #fff !important; 54 | } 55 | .Title{ 56 | margin-top: 16px; 57 | min-height: 62px; 58 | } 59 | .Title p{ 60 | margin: 0px; 61 | color: var(--text-white); 62 | } 63 | .Title p:last-child{ 64 | color: rgb(179, 179, 179); 65 | margin-top: 4px; 66 | -webkit-line-clamp: 2; 67 | } 68 | .IconBox button:hover{ 69 | -webkit-transform: scale(1.06); 70 | transform: scale(1.06); 71 | } 72 | .PlaylistCardSBox:hover .IconBox{ 73 | bottom: 106px; 74 | opacity: 1; 75 | } -------------------------------------------------------------------------------- /src/component/cards/playlist-card-s.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { connect } from 'react-redux'; 3 | import { changeTrack } from '../../actions'; 4 | import { Link } from "react-router-dom"; 5 | import TextBoldL from '../text/text-bold-l'; 6 | import PlayButton from '../buttons/play-button'; 7 | 8 | import styles from "./playlist-card-s.module.css"; 9 | 10 | function PlaylistCardS(props){ 11 | const[isthisplay, setIsthisPlay] = useState(false) 12 | 13 | function changeTheme(){ 14 | document.documentElement.style.setProperty('--hover-home-bg', props.data.hoverColor); 15 | } 16 | 17 | useEffect(() => { 18 | setIsthisPlay(parseInt(props.data.index) === props.trackData.trackKey[0]) 19 | }) 20 | 21 | return ( 22 |
23 | 24 |
25 |
26 | {`${props.data.title}`} 27 |
28 |
29 | {props.data.title} 30 |
31 |
32 | 33 |
props.changeTrack([parseInt(props.data.index), 0])} 35 | className={`${styles.IconBox} ${isthisplay&&props.isPlaying ? styles.ActiveIconBox : ''}`} 36 | > 37 | 38 |
39 |
40 | ); 41 | } 42 | 43 | const mapStateToProps = (state) => { 44 | return { 45 | trackData: state.trackData, 46 | isPlaying: state.isPlaying 47 | }; 48 | }; 49 | 50 | export default connect(mapStateToProps, { changeTrack })(PlaylistCardS); -------------------------------------------------------------------------------- /src/component/cards/playlist-card-s.module.css: -------------------------------------------------------------------------------- 1 | .PlaylistCardSBox{ 2 | position: relative; 3 | } 4 | .PlaylistCardS{ 5 | position: relative; 6 | height: 80px; 7 | background-color: hsla(0,0%,100%,.1); 8 | border-radius: 4px; 9 | -webkit-transition: background-color .3s ease; 10 | display: grid; 11 | grid-template-columns: 80px 1fr; 12 | } 13 | .ImgBox{ 14 | width: 80px; 15 | height: 80px; 16 | } 17 | .ImgBox img{ 18 | width: 100%; 19 | height: 100%; 20 | border-radius: 4px 0px 0px 4px; 21 | } 22 | .Title{ 23 | display: flex; 24 | align-items: center; 25 | justify-content: space-between; 26 | padding: 0px 16px; 27 | color: #fff; 28 | } 29 | .Title p { 30 | -webkit-line-clamp: 2; 31 | word-break: break-word; 32 | } 33 | .IconBox{ 34 | display: inline-flex; 35 | opacity: 0; 36 | transition: all .3s ease; 37 | box-shadow: 0 8px 8px rgb(0 0 0 / 30%); 38 | z-index: 2; 39 | border-radius: 500px; 40 | position: absolute; 41 | right: 16px; 42 | top: calc(50% - 20px); 43 | } 44 | .IconBox div{ 45 | background-color: #1db954; 46 | height: 40px; 47 | width: 40px; 48 | } 49 | .IconBox button{ 50 | height: 40px; 51 | width: 40px; 52 | -webkit-transition: -webkit-transform 33ms cubic-bezier(.3,0,0,1); 53 | transition: -webkit-transform 33ms cubic-bezier(.3,0,0,1); 54 | transition: transform 33ms cubic-bezier(.3,0,0,1); 55 | transition: transform 33ms cubic-bezier(.3,0,0,1),-webkit-transform 33ms cubic-bezier(.3,0,0,1); 56 | } 57 | .ActiveIconBox{ 58 | opacity: 1; 59 | } 60 | .IconBox button svg{ 61 | fill: #fff !important; 62 | } 63 | .IconBox button:hover{ 64 | -webkit-transform: scale(1.06); 65 | transform: scale(1.06); 66 | } 67 | .PlaylistCardS:hover { 68 | background: hsla(0,0%,100%,.2); 69 | -webkit-backdrop-filter: blur(60px); 70 | backdrop-filter: blur(60px); 71 | } 72 | .PlaylistCardSBox:hover .IconBox{ 73 | opacity: 1; 74 | } -------------------------------------------------------------------------------- /src/component/cards/searchpage-card.js: -------------------------------------------------------------------------------- 1 | import TitleM from '../text/title-m' 2 | 3 | import styles from "./searchpage-card.module.css"; 4 | 5 | function SearchPageCard({cardData}){ 6 | return ( 7 |
8 |
9 | 10 | {cardData.title} 11 |
12 |
13 | ); 14 | } 15 | 16 | export default SearchPageCard; -------------------------------------------------------------------------------- /src/component/cards/searchpage-card.module.css: -------------------------------------------------------------------------------- 1 | .SearchCardBox{ 2 | overflow: hidden; 3 | position: relative; 4 | width: 100%; 5 | border: none; 6 | border-radius: 8px; 7 | } 8 | .SearchCardBox::after { 9 | content: ""; 10 | display: block; 11 | padding-bottom: 100%; 12 | } 13 | .SearchCard{ 14 | position: absolute; 15 | width: 100%; 16 | height: 100%; 17 | background: -webkit-gradient(linear,left bottom,left top,from(transparent),to(rgba(0,0,0,.4))); 18 | background: linear-gradient(0deg,transparent,rgba(0,0,0,.4)); 19 | } 20 | .SearchCard img{ 21 | width: 100px; 22 | height: 100px; 23 | position: absolute; 24 | bottom: 0; 25 | right: 0; 26 | -webkit-box-shadow: 0 2px 4px 0 rgb(0 0 0 / 20%); 27 | box-shadow: 0 2px 4px 0 rgb(0 0 0 / 20%); 28 | -webkit-transform: rotate(25deg) translate(18%,-2%); 29 | transform: rotate(25deg) translate(18%,-2%);} 30 | .SearchCard h2{ 31 | padding: 16px; 32 | margin: 0; 33 | top: 0; 34 | letter-spacing: -.04em; 35 | overflow-wrap: break-word; 36 | position: absolute; 37 | } -------------------------------------------------------------------------------- /src/component/footer/audio.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Audio = forwardRef(({ trackData, handleDuration, handleCurrentTime, isPlaying }, ref) => { 5 | return ( 6 |