├── .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 |
44 |
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 |

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 |

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 |