├── .gitignore
├── postcss.config.js
├── src
├── assets
│ ├── index.js
│ ├── constants.js
│ ├── loader.svg
│ ├── macFavicon.svg
│ └── macLogo.svg
├── components
│ ├── Error.jsx
│ ├── Loader.jsx
│ ├── PlayPause.jsx
│ ├── ArtistCard.jsx
│ ├── index.js
│ ├── MusicPlayer
│ │ ├── Track.jsx
│ │ ├── VolumeBar.jsx
│ │ ├── Player.jsx
│ │ ├── Seekbar.jsx
│ │ ├── Controls.jsx
│ │ └── index.jsx
│ ├── RelatedSongs.jsx
│ ├── Searchbar.jsx
│ ├── DetailsHeader.jsx
│ ├── SongBar.jsx
│ ├── SongCard.jsx
│ ├── Sidebar.jsx
│ └── TopPlay.jsx
├── pages
│ ├── index.js
│ ├── TopArtists.jsx
│ ├── ArtistDetails.jsx
│ ├── TopCharts.jsx
│ ├── Search.jsx
│ ├── AroundYou.jsx
│ ├── Discover.jsx
│ └── SongDetails.jsx
├── redux
│ ├── store.js
│ ├── services
│ │ └── shazamCore.js
│ └── features
│ │ └── playerSlice.js
├── index.jsx
├── index.css
└── App.jsx
├── vite.config.js
├── index.html
├── package.json
├── tailwind.config.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/assets/index.js:
--------------------------------------------------------------------------------
1 | import loader from './loader.svg';
2 | import logo from './macLogo.svg';
3 |
4 | export {
5 | logo,
6 | loader,
7 | };
8 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | });
8 |
--------------------------------------------------------------------------------
/src/components/Error.jsx:
--------------------------------------------------------------------------------
1 | const Error = () => (
2 |
3 |
Something went wrong. Please try again.
4 |
5 | );
6 |
7 | export default Error;
8 |
--------------------------------------------------------------------------------
/src/components/Loader.jsx:
--------------------------------------------------------------------------------
1 | import { loader } from '../assets';
2 |
3 | const Loader = ({ title }) => (
4 |
5 |
6 |
{title || 'Loading...'}
7 |
8 | );
9 |
10 | export default Loader;
11 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import Discover from './Discover';
2 | import TopArtists from './TopArtists';
3 | import ArtistDetails from './ArtistDetails';
4 | import SongDetails from './SongDetails';
5 | import Search from './Search';
6 | import TopCharts from './TopCharts';
7 | import AroundYou from './AroundYou';
8 |
9 | export {
10 | Discover,
11 | Search,
12 | TopArtists,
13 | ArtistDetails,
14 | SongDetails,
15 | TopCharts,
16 | AroundYou,
17 | };
18 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 |
3 | import playerReducer from './features/playerSlice';
4 | import { shazamCoreApi } from './services/shazamCore';
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | [shazamCoreApi.reducerPath]: shazamCoreApi.reducer,
9 | player: playerReducer,
10 | },
11 | middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(shazamCoreApi.middleware),
12 | });
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Music App Clone
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/PlayPause.jsx:
--------------------------------------------------------------------------------
1 | import { FaPauseCircle, FaPlayCircle } from "react-icons/fa";
2 |
3 | const PlayPause = ({isPlaying, activeSong, song, handlePause, handlePlay}) => (isPlaying && activeSong?.title === song.title ? (
4 |
9 | ) : (
10 |
15 | ));
16 |
17 | export default PlayPause;
18 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter as Router } from 'react-router-dom';
4 | import { Provider } from 'react-redux';
5 |
6 | import './index.css';
7 | import App from './App';
8 | import { store } from './redux/store';
9 |
10 | ReactDOM.createRoot(document.getElementById('root')).render(
11 |
12 |
13 |
14 |
15 |
16 |
17 | ,
18 | );
19 |
--------------------------------------------------------------------------------
/src/components/ArtistCard.jsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router-dom";
2 |
3 | const ArtistCard = ({ track }) => {
4 | const navigate = useNavigate();
5 |
6 | return (
7 | navigate(`/artists/${track?.artists[0].adamid}`)}>
8 |
9 |
{track?.subtitle}
10 |
11 | );
12 | };
13 |
14 | export default ArtistCard;
15 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | import Sidebar from './Sidebar';
2 | import Searchbar from './Searchbar';
3 | import SongCard from './SongCard';
4 | import TopPlay from './TopPlay';
5 | import ArtistCard from './ArtistCard';
6 | import DetailsHeader from './DetailsHeader';
7 | import SongBar from './SongBar';
8 | import RelatedSongs from './RelatedSongs';
9 | import MusicPlayer from './MusicPlayer';
10 | import Loader from './Loader';
11 | import Error from './Error';
12 |
13 | export {
14 | TopPlay,
15 | Sidebar,
16 | SongCard,
17 | Searchbar,
18 | ArtistCard,
19 | DetailsHeader,
20 | SongBar,
21 | RelatedSongs,
22 | MusicPlayer,
23 | Loader,
24 | Error,
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/MusicPlayer/Track.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Track = ({ isPlaying, isActive, activeSong }) => (
4 |
5 |
6 |
7 |
8 |
9 |
10 | {activeSong?.title ? activeSong?.title : 'No active Song'}
11 |
12 |
13 | {activeSong?.subtitle ? activeSong?.subtitle : 'No active Song'}
14 |
15 |
16 |
17 | );
18 |
19 | export default Track;
20 |
--------------------------------------------------------------------------------
/src/components/RelatedSongs.jsx:
--------------------------------------------------------------------------------
1 | import SongBar from "./SongBar";
2 |
3 |
4 | const RelatedSongs = ({ data, isPlaying, activeSong, handlePauseClick, handlePlayClick, artistId }) => (
5 |
6 |
Related Songs:
7 |
8 |
9 | {data?.map((song, i) => (
10 |
20 | ))}
21 |
22 |
23 |
24 | );
25 |
26 | export default RelatedSongs;
27 |
--------------------------------------------------------------------------------
/src/components/MusicPlayer/VolumeBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BsFillVolumeUpFill, BsVolumeDownFill, BsFillVolumeMuteFill } from 'react-icons/bs';
3 |
4 | const VolumeBar = ({ value, min, max, onChange, setVolume }) => (
5 |
6 | {value <= 1 && value > 0.5 && setVolume(0)} />}
7 | {value <= 0.5 && value > 0 && setVolume(0)} />}
8 | {value === 0 && setVolume(1)} />}
9 |
18 |
19 | );
20 |
21 | export default VolumeBar;
22 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* Active Navigation Link */
6 | .active {
7 | @apply text-white;
8 | }
9 |
10 | .active svg {
11 | @apply text-white;
12 | }
13 |
14 | .bar:nth-child(2) {
15 | animation-delay: 0.2s;
16 | }
17 | .bar:nth-child(3) {
18 | animation-delay: 0.4s;
19 | }
20 | .bar:nth-child(4) {
21 | animation-delay: 0.6s;
22 | }
23 | .bar:nth-child(5) {
24 | animation-delay: 0.8s;
25 | }
26 | .bar:nth-child(6) {
27 | animation-delay: 1s;
28 | }
29 |
30 | .smooth-transition {
31 | transition: all 0.3s ease-in-out;
32 | }
33 |
34 | /* Hide scrollbar for Chrome, Safari and Opera */
35 | .hide-scrollbar::-webkit-scrollbar {
36 | display: none;
37 | }
38 |
39 | /* Hide scrollbar for IE, Edge and Firefox */
40 | .hide-scrollbar {
41 | -ms-overflow-style: none; /* IE and Edge */
42 | scrollbar-width: none; /* Firefox */
43 | }
44 |
--------------------------------------------------------------------------------
/src/pages/TopArtists.jsx:
--------------------------------------------------------------------------------
1 | import { ArtistCard, Loader, Error } from "../components";
2 | import { useGetTopChartsQuery } from "../redux/services/shazamCore";
3 |
4 | const TopArtists = () => {
5 | const { data, isFetching, error } = useGetTopChartsQuery();
6 |
7 | if(isFetching) return ;
8 |
9 | if(error) return ;
10 |
11 | return (
12 |
13 |
14 | Top Artists
15 |
16 |
17 |
18 | {data?.map((track) => (
19 |
23 | ))}
24 |
25 |
26 | )
27 |
28 | };
29 |
30 | export default TopArtists;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elagant-music-app",
3 | "private": true,
4 | "version": "0.0.0",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview"
9 | },
10 | "dependencies": {
11 | "@reduxjs/toolkit": "^1.8.5",
12 | "axios": "^0.27.2",
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-icons": "^4.4.0",
16 | "react-redux": "^8.0.2",
17 | "react-router-dom": "^6.3.0",
18 | "swiper": "^8.4.2"
19 | },
20 | "devDependencies": {
21 | "@types/react": "^18.0.0",
22 | "@types/react-dom": "^18.0.0",
23 | "@vitejs/plugin-react": "^1.3.0",
24 | "autoprefixer": "^10.4.7",
25 | "eslint": "^8.18.0",
26 | "eslint-config-airbnb": "^19.0.4",
27 | "eslint-plugin-import": "^2.26.0",
28 | "eslint-plugin-jsx-a11y": "^6.5.1",
29 | "eslint-plugin-react": "^7.30.0",
30 | "eslint-plugin-react-hooks": "^4.6.0",
31 | "postcss": "^8.4.14",
32 | "tailwindcss": "^3.1.3",
33 | "vite": "^2.9.9"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/MusicPlayer/Player.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/media-has-caption */
2 | import React, { useRef, useEffect } from 'react';
3 |
4 | const Player = ({ activeSong, isPlaying, volume, seekTime, onEnded, onTimeUpdate, onLoadedData, repeat }) => {
5 | const ref = useRef(null);
6 | // eslint-disable-next-line no-unused-expressions
7 | if (ref.current) {
8 | if (isPlaying) {
9 | ref.current.play();
10 | } else {
11 | ref.current.pause();
12 | }
13 | }
14 |
15 | useEffect(() => {
16 | ref.current.volume = volume;
17 | }, [volume]);
18 | // updates audio element only on seekTime change (and not on each rerender):
19 | useEffect(() => {
20 | ref.current.currentTime = seekTime;
21 | }, [seekTime]);
22 |
23 | return (
24 |
32 | );
33 | };
34 |
35 | export default Player;
36 |
--------------------------------------------------------------------------------
/src/assets/constants.js:
--------------------------------------------------------------------------------
1 | import { HiOutlineHashtag, HiOutlineHome, HiOutlinePhotograph, HiOutlineUserGroup } from 'react-icons/hi';
2 |
3 | export const genres = [
4 | { title: 'Pop', value: 'POP' },
5 | { title: 'Hip-Hop', value: 'HIP_HOP_RAP' },
6 | { title: 'Dance', value: 'DANCE' },
7 | { title: 'Electronic', value: 'ELECTRONIC' },
8 | { title: 'Soul', value: 'SOUL_RNB' },
9 | { title: 'Alternative', value: 'ALTERNATIVE' },
10 | { title: 'Rock', value: 'ROCK' },
11 | { title: 'Latin', value: 'LATIN' },
12 | { title: 'Film', value: 'FILM_TV' },
13 | { title: 'Country', value: 'COUNTRY' },
14 | { title: 'Worldwide', value: 'WORLDWIDE' },
15 | { title: 'Reggae', value: 'REGGAE_DANCE_HALL' },
16 | { title: 'House', value: 'HOUSE' },
17 | { title: 'K-Pop', value: 'K_POP' },
18 | ];
19 |
20 | export const links = [
21 | { name: 'Discover', to: '/', icon: HiOutlineHome },
22 | { name: 'Around You', to: '/around-you', icon: HiOutlinePhotograph },
23 | { name: 'Top Artists', to: '/top-artists', icon: HiOutlineUserGroup },
24 | { name: 'Top Charts', to: '/top-charts', icon: HiOutlineHashtag },
25 | ];
26 |
--------------------------------------------------------------------------------
/src/components/MusicPlayer/Seekbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Seekbar = ({ value, min, max, onInput, setSeekTime, appTime }) => {
4 | // converts the time to format 0:00
5 | const getTime = (time) => `${Math.floor(time / 60)}:${(`0${Math.floor(time % 60)}`).slice(-2)}`;
6 |
7 | return (
8 |
9 |
setSeekTime(appTime - 5)} className="hidden lg:mr-4 lg:block text-white">
10 | -
11 |
12 |
{value === 0 ? '0:00' : getTime(value)}
13 |
22 |
{max === 0 ? '0:00' : getTime(max)}
23 |
setSeekTime(appTime + 5)} className="hidden lg:ml-4 lg:block text-white">
24 | +
25 |
26 |
27 | );
28 | };
29 |
30 | export default Seekbar;
31 |
--------------------------------------------------------------------------------
/src/pages/ArtistDetails.jsx:
--------------------------------------------------------------------------------
1 | import { useParams } from "react-router-dom";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import { DetailsHeader, Error, Loader, RelatedSongs } from "../components";
4 |
5 | import { useGetArtistDetailsQuery } from "../redux/services/shazamCore";
6 |
7 | const ArtistDetails = () => {
8 | const { id: artistId } = useParams();
9 | const { activeSong, isPlaying } = useSelector((state) => state.player);
10 | const {
11 | data: artistData,
12 | isFetching: isFetchingArtistDetails,
13 | error,
14 | } = useGetArtistDetailsQuery(artistId);
15 |
16 | if (isFetchingArtistDetails) return ;
17 |
18 | if (error) return ;
19 |
20 | return (
21 |
22 |
26 |
27 |
33 |
34 | );
35 | };
36 |
37 | export default ArtistDetails;
38 |
--------------------------------------------------------------------------------
/src/components/Searchbar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import { useNavigate } from "react-router-dom";
4 |
5 | import { FiSearch } from "react-icons/fi";
6 |
7 | const Searchbar = () => {
8 | const navigate = useNavigate();
9 | const [searchTerm, setSearchTerm] = useState('');
10 |
11 | const handleSubmit = (e) => {
12 | e.preventDefault();
13 |
14 | navigate(`/search/${searchTerm}`);
15 | };
16 |
17 | return (
18 |
36 | );
37 | };
38 |
39 | export default Searchbar;
40 |
--------------------------------------------------------------------------------
/src/components/MusicPlayer/Controls.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { MdSkipNext, MdSkipPrevious } from 'react-icons/md';
3 | import { BsArrowRepeat, BsFillPauseFill, BsFillPlayFill, BsShuffle } from 'react-icons/bs';
4 |
5 | const Controls = ({ isPlaying, repeat, setRepeat, shuffle, setShuffle, currentSongs, handlePlayPause, handlePrevSong, handleNextSong }) => (
6 |
7 | setRepeat((prev) => !prev)} className="hidden sm:block cursor-pointer" />
8 | {currentSongs?.length && }
9 | {isPlaying ? (
10 |
11 | ) : (
12 |
13 | )}
14 | {currentSongs?.length && }
15 | setShuffle((prev) => !prev)} className="hidden sm:block cursor-pointer" />
16 |
17 | );
18 |
19 | export default Controls;
20 |
--------------------------------------------------------------------------------
/src/pages/TopCharts.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 |
3 | import { Error, Loader, SongCard } from "../components";
4 | import { useGetTopChartsQuery } from "../redux/services/shazamCore";
5 |
6 | const TopCharts = () => {
7 | const { activeSong, isPlaying } = useSelector((state) => state.player);
8 | const { data, isFetching, error } = useGetTopChartsQuery();
9 |
10 | if(isFetching) return ;
11 |
12 | if(error) return ;
13 |
14 | return (
15 |
16 |
17 | Discover Top Charts
18 |
19 |
20 |
21 | {data?.map((song, i) => (
22 |
30 | ))}
31 |
32 |
33 | )
34 |
35 | };
36 |
37 | export default TopCharts;
38 |
--------------------------------------------------------------------------------
/src/redux/services/shazamCore.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2 |
3 | export const shazamCoreApi = createApi({
4 | reducerPath: 'shazamCoreApi',
5 | baseQuery: fetchBaseQuery({
6 | baseUrl: 'https://shazam-core.p.rapidapi.com/v1',
7 | prepareHeaders: (headers) => {
8 | headers.set('X-RapidAPI-Key', '2b57e7d6cfmsh4143f4af5d9a501p1d7c37jsnb5cf21f61c5c');
9 |
10 | return headers;
11 | },
12 | }),
13 | endpoints: (builder) => ({
14 | getTopCharts: builder.query({ query: () => '/charts/world' }),
15 | getSongsByGenre: builder.query({ query:( genre ) => `/charts/genre-world?genre_code=${genre}` }),
16 | getSongDetails: builder.query({ query:({ songid }) => `/tracks/details?track_id=${songid}` }),
17 | getSongRelated: builder.query({ query:({ songid }) => `/tracks/related?track_id=${songid}` }),
18 | getArtistDetails: builder.query({ query:( artistId ) => `/artists/details?artist_id=${artistId}` }),
19 | getSongsByCountry: builder.query({ query:( countryCode ) => `/charts/country?country_code=${countryCode}` }),
20 | getSongsBySearch: builder.query({ query:( searchTerm ) => `/search/multi?search_type=SONGS_ARTISTS&query=${searchTerm}` }),
21 | }),
22 | });
23 |
24 | export const {
25 | useGetTopChartsQuery,
26 | useGetSongsByGenreQuery,
27 | useGetSongDetailsQuery,
28 | useGetSongRelatedQuery,
29 | useGetArtistDetailsQuery,
30 | useGetSongsByCountryQuery,
31 | useGetSongsBySearchQuery,
32 | } = shazamCoreApi;
--------------------------------------------------------------------------------
/src/components/DetailsHeader.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | const DetailsHeader = ({ artistId, artistData, songData }) => {
4 | const artist = artistData?.artists[artistId]?.attributes;
5 |
6 | return (
7 |
8 |
9 |
10 |
11 |
20 |
21 |
22 |
{artistId ? artist?.name : songData?.title}
23 | {!artistId && (
24 |
25 |
{songData?.subtitle}
26 |
27 | )}
28 |
29 |
{artistId ? artist?.genreNames[0] : songData?.genres?.primary}
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default DetailsHeader;
39 |
--------------------------------------------------------------------------------
/src/pages/Search.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { useParams } from "react-router-dom";
3 |
4 | import { Error, Loader, SongCard } from "../components";
5 | import { useGetSongsBySearchQuery } from "../redux/services/shazamCore";
6 |
7 | const Search = () => {
8 | const { searchTerm } = useParams();
9 | const { activeSong, isPlaying } = useSelector((state) => state.player);
10 | const { data, isFetching, error } = useGetSongsBySearchQuery(searchTerm);
11 |
12 | const songs = data?.tracks?.hits?.map((song) => song.track);
13 |
14 | if(isFetching) return ;
15 |
16 | if(error) return ;
17 |
18 | return (
19 |
20 |
21 | Showing results for {searchTerm}
22 |
23 |
24 |
25 | {songs?.map((song, i) => (
26 |
34 | ))}
35 |
36 |
37 | )
38 |
39 | };
40 |
41 | export default Search;
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {
6 | colors: {
7 | black: '#191624',
8 | },
9 | animation: {
10 | slideup: 'slideup 1s ease-in-out',
11 | slidedown: 'slidedown 1s ease-in-out',
12 | slideleft: 'slideleft 1s ease-in-out',
13 | slideright: 'slideright 1s ease-in-out',
14 | wave: 'wave 1.2s linear infinite',
15 | slowfade: 'slowfade 2.2s ease-in-out',
16 | },
17 | keyframes: {
18 | slowfade: {
19 | from: { opacity: 0 },
20 | to: { opacity: 1 },
21 | },
22 | slideup: {
23 | from: { opacity: 0, transform: 'translateY(25%)' },
24 | to: { opacity: 1, transform: 'none' },
25 | },
26 | slidedown: {
27 | from: { opacity: 0, transform: 'translateY(-25%)' },
28 | to: { opacity: 1, transform: 'none' },
29 | },
30 | slideleft: {
31 | from: { opacity: 0, transform: 'translateX(-20px)' },
32 | to: { opacity: 1, transform: 'translateX(0)' },
33 | },
34 | slideright: {
35 | from: { opacity: 0, transform: 'translateX(20px)' },
36 | to: { opacity: 1, transform: 'translateX(0)' },
37 | },
38 | wave: {
39 | '0%': { transform: 'scale(0)' },
40 | '50%': { transform: 'scale(1)' },
41 | '100%': { transform: 'scale(0)' },
42 | },
43 | },
44 | },
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/SongBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import PlayPause from './PlayPause';
5 |
6 | const SongBar = ({ song, i, artistId, isPlaying, activeSong, handlePauseClick, handlePlayClick }) => (
7 |
8 |
{i + 1}.
9 |
10 |
15 |
16 | {!artistId ? (
17 |
18 |
19 | {song?.title}
20 |
21 |
22 | ) : (
23 |
24 | {song?.attributes?.name}
25 |
26 | )}
27 |
28 | {artistId ? song?.attributes?.albumName : song?.subtitle}
29 |
30 |
31 |
32 | {!artistId
33 | ? (
34 |
handlePlayClick(song, i)}
40 | />
41 | )
42 | : null}
43 |
44 | );
45 |
46 | export default SongBar;
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { Route, Routes } from 'react-router-dom';
3 |
4 | import { Searchbar, Sidebar, MusicPlayer, TopPlay } from './components';
5 | import { ArtistDetails, TopArtists, AroundYou, Discover, Search, SongDetails, TopCharts } from './pages';
6 |
7 | const App = () => {
8 | const { activeSong } = useSelector((state) => state.player);
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | } />
20 | } />
21 | } />
22 | } />
23 | } />
24 | } />
25 | } />
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {activeSong?.title && (
35 |
36 |
37 |
38 | )}
39 |
40 | );
41 | };
42 |
43 | export default App;
44 |
--------------------------------------------------------------------------------
/src/pages/AroundYou.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import axios from "axios";
3 | import { useSelector } from "react-redux";
4 |
5 | import { Error, Loader, SongCard } from "../components";
6 | import { useGetSongsByCountryQuery } from "../redux/services/shazamCore";
7 |
8 | const AroundYou = () => {
9 | const [country, setCountry] = useState('');
10 | const [loading, setloading] = useState(true);
11 | const { activeSong, isPlaying } = useSelector((state) => state.player);
12 | const { data, isFetching, error } = useGetSongsByCountryQuery(country);
13 |
14 | useEffect(() => {
15 | axios.get(`https://geo.ipify.org/api/v2/country?apiKey=at_nxOsushE5XT1Zrw6ULvn8uIRtTrOU`)
16 | .then((res) => setCountry(res?.data?.location?.country))
17 | .catch((err) => console.log(err))
18 | .finally(() => setloading(false));
19 | }, [country]);
20 |
21 | if(isFetching && loading) return ;
22 |
23 | if(error && country) return ;
24 |
25 | return (
26 |
27 |
28 | Around You {country}
29 |
30 |
31 |
32 | {data?.map((song, i) => (
33 |
41 | ))}
42 |
43 |
44 | )
45 |
46 | };
47 |
48 | export default AroundYou;
49 |
--------------------------------------------------------------------------------
/src/components/SongCard.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import { useDispatch } from "react-redux";
3 |
4 | import PlayPause from "./PlayPause";
5 | import { playPause, setActiveSong } from "../redux/features/playerSlice";
6 |
7 | const SongCard = ({ song, isPlaying, activeSong, i, data }) => {
8 | const dispatch = useDispatch();
9 |
10 | const handlePauseClick = () => {
11 | dispatch(playPause(false));
12 | }
13 |
14 | const handlePlayClick = () => {
15 | dispatch(setActiveSong({ song, data, i}));
16 | dispatch(playPause(true));
17 | }
18 |
19 | return (
20 |
21 |
22 |
35 |
36 |
37 |
38 |
49 |
50 | );
51 | };
52 |
53 | export default SongCard;
54 |
--------------------------------------------------------------------------------
/src/pages/Discover.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 |
3 | import { Error, Loader, SongCard } from "../components";
4 | import { genres } from "../assets/constants";
5 |
6 | import { useGetSongsByGenreQuery } from "../redux/services/shazamCore";
7 | import { selectGenreListId } from "../redux/features/playerSlice";
8 |
9 | const Discover = () => {
10 | const dispatch = useDispatch();
11 | const { activeSong, isPlaying, genreListId } = useSelector((state) => state.player);
12 | const { data, isFetching, error } = useGetSongsByGenreQuery(genreListId || 'POP');
13 |
14 | if (isFetching) return ;
15 |
16 | if (error) return ;
17 |
18 | const genreTitle = genres.find(({value}) => value === genreListId)?.title;
19 |
20 | return (
21 |
22 |
23 |
Discover {genreTitle}
24 | {dispatch(selectGenreListId(e.target.value))}}
26 | value={genreListId || 'pop'}
27 | className="bg-black text-gray-300 p-3 text-sm rounded-lg outline-none sm:mt-0 mt-5"
28 | >
29 | {genres.map((genre) => (
30 |
31 | {genre.title}
32 |
33 | ))}
34 |
35 |
36 |
37 |
38 | {data?.map((song, i) => (
39 |
46 | ))}
47 |
48 |
49 | );
50 | };
51 |
52 | export default Discover;
53 |
--------------------------------------------------------------------------------
/src/redux/features/playerSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | currentSongs: [],
5 | currentIndex: 0,
6 | isActive: false,
7 | isPlaying: false,
8 | activeSong: {},
9 | genreListId: '',
10 | };
11 |
12 | const playerSlice = createSlice({
13 | name: 'player',
14 | initialState,
15 | reducers: {
16 | setActiveSong: (state, action) => {
17 | state.activeSong = action.payload.song;
18 |
19 | if (action.payload?.data?.tracks?.hits) {
20 | state.currentSongs = action.payload.data.tracks.hits;
21 | } else if (action.payload?.data?.properties) {
22 | state.currentSongs = action.payload?.data?.tracks;
23 | } else {
24 | state.currentSongs = action.payload.data;
25 | }
26 |
27 | state.currentIndex = action.payload.i;
28 | state.isActive = true;
29 | },
30 |
31 | nextSong: (state, action) => {
32 | if (state.currentSongs[action.payload]?.track) {
33 | state.activeSong = state.currentSongs[action.payload]?.track;
34 | } else {
35 | state.activeSong = state.currentSongs[action.payload];
36 | }
37 |
38 | state.currentIndex = action.payload;
39 | state.isActive = true;
40 | },
41 |
42 | prevSong: (state, action) => {
43 | if (state.currentSongs[action.payload]?.track) {
44 | state.activeSong = state.currentSongs[action.payload]?.track;
45 | } else {
46 | state.activeSong = state.currentSongs[action.payload];
47 | }
48 |
49 | state.currentIndex = action.payload;
50 | state.isActive = true;
51 | },
52 |
53 | playPause: (state, action) => {
54 | state.isPlaying = action.payload;
55 | },
56 |
57 | selectGenreListId: (state, action) => {
58 | state.genreListId = action.payload;
59 | },
60 | },
61 | });
62 |
63 | export const { setActiveSong, nextSong, prevSong, playPause, selectGenreListId } = playerSlice.actions;
64 |
65 | export default playerSlice.reducer;
66 |
--------------------------------------------------------------------------------
/src/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { NavLink } from "react-router-dom";
3 | import { HiOutlineMenu } from "react-icons/hi";
4 | import { RiCloseLine } from "react-icons/ri";
5 |
6 | import { logo } from "../assets";
7 | import { links } from "../assets/constants";
8 |
9 | const NavLinks = ({ handleClick }) => (
10 |
11 | {links.map((item) => (
12 | handleClick && handleClick()}
17 | >
18 |
19 | {item.name}
20 |
21 | ))}
22 |
23 | );
24 |
25 | const Sidebar = () => {
26 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
27 |
28 | return (
29 | <>
30 |
31 |
32 |
Music App Clone
33 |
34 |
35 |
36 |
37 | {mobileMenuOpen ? (
38 | setMobileMenuOpen(false)} />
39 | ): setMobileMenuOpen(true)} />}
40 |
41 |
42 |
43 |
44 |
Music App Clone
45 |
setMobileMenuOpen(false)} />
46 |
47 | >
48 | );
49 | };
50 |
51 | export default Sidebar;
52 |
--------------------------------------------------------------------------------
/src/pages/SongDetails.jsx:
--------------------------------------------------------------------------------
1 | import { useParams } from "react-router-dom";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import { DetailsHeader, Error, Loader, RelatedSongs } from "../components";
4 |
5 | import { setActiveSong, playPause } from "../redux/features/playerSlice";
6 | import { useGetSongDetailsQuery, useGetSongRelatedQuery } from "../redux/services/shazamCore";
7 |
8 | const SongDetails = () => {
9 | const dispatch = useDispatch();
10 | const { songid } = useParams();
11 | const { activeSong, isPlaying } = useSelector((state) => state.player);
12 | const { data: songData, isFetching: isFetchingSongDetails } = useGetSongDetailsQuery({ songid });
13 | const { data, isFetching: isFetchingRelatedSongs, error } = useGetSongRelatedQuery({ songid });
14 |
15 | const handlePauseClick = () => {
16 | dispatch(playPause(false));
17 | };
18 |
19 | const handlePlayClick = (song, i) => {
20 | dispatch(setActiveSong({ song, data, i }));
21 | dispatch(playPause(true));
22 | };
23 |
24 | if (isFetchingSongDetails || isFetchingRelatedSongs)
25 | return ;
26 |
27 | if (error) return ;
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
Lyrics:
35 |
36 |
37 | {songData?.sections[1].type === "LYRICS" ? (
38 | songData?.sections[1].text.map((line, i) => (
39 |
{line}
40 | ))
41 | ) : (
42 |
Sorry, no lyrics found!
43 | )}
44 |
45 |
46 |
47 |
54 |
55 | );
56 | };
57 |
58 | export default SongDetails;
59 |
--------------------------------------------------------------------------------
/src/components/MusicPlayer/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 |
4 | import { nextSong, prevSong, playPause } from '../../redux/features/playerSlice';
5 | import Controls from './Controls';
6 | import Player from './Player';
7 | import Seekbar from './Seekbar';
8 | import Track from './Track';
9 | import VolumeBar from './VolumeBar';
10 |
11 | const MusicPlayer = () => {
12 | const { activeSong, currentSongs, currentIndex, isActive, isPlaying } = useSelector((state) => state.player);
13 | const [duration, setDuration] = useState(0);
14 | const [seekTime, setSeekTime] = useState(0);
15 | const [appTime, setAppTime] = useState(0);
16 | const [volume, setVolume] = useState(0.3);
17 | const [repeat, setRepeat] = useState(false);
18 | const [shuffle, setShuffle] = useState(false);
19 | const dispatch = useDispatch();
20 |
21 | useEffect(() => {
22 | if (currentSongs.length) dispatch(playPause(true));
23 | }, [currentIndex]);
24 |
25 | const handlePlayPause = () => {
26 | if (!isActive) return;
27 |
28 | if (isPlaying) {
29 | dispatch(playPause(false));
30 | } else {
31 | dispatch(playPause(true));
32 | }
33 | };
34 |
35 | const handleNextSong = () => {
36 | dispatch(playPause(false));
37 |
38 | if (!shuffle) {
39 | dispatch(nextSong((currentIndex + 1) % currentSongs.length));
40 | } else {
41 | dispatch(nextSong(Math.floor(Math.random() * currentSongs.length)));
42 | }
43 | };
44 |
45 | const handlePrevSong = () => {
46 | if (currentIndex === 0) {
47 | dispatch(prevSong(currentSongs.length - 1));
48 | } else if (shuffle) {
49 | dispatch(prevSong(Math.floor(Math.random() * currentSongs.length)));
50 | } else {
51 | dispatch(prevSong(currentIndex - 1));
52 | }
53 | };
54 |
55 | return (
56 |
57 |
58 |
59 |
71 |
setSeekTime(event.target.value)}
76 | setSeekTime={setSeekTime}
77 | appTime={appTime}
78 | />
79 | setAppTime(event.target.currentTime)}
88 | onLoadedData={(event) => setDuration(event.target.duration)}
89 | />
90 |
91 |
setVolume(event.target.value)} setVolume={setVolume} />
92 |
93 | );
94 | };
95 |
96 | export default MusicPlayer;
97 |
--------------------------------------------------------------------------------
/src/components/TopPlay.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import { Link } from "react-router-dom";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import { Swiper, SwiperSlide } from "swiper/react";
5 | import { FreeMode } from "swiper";
6 |
7 | import PlayPause from "./PlayPause";
8 | import { playPause, setActiveSong } from "../redux/features/playerSlice";
9 | import { useGetTopChartsQuery } from "../redux/services/shazamCore";
10 |
11 | import "swiper/css";
12 | import "swiper/css/free-mode";
13 |
14 | const TopChartCard = ({ song, i, isPlaying, activeSong, handlePauseClick, handlePlayClick }) => (
15 |
16 |
{i + 1}.
17 |
18 |
19 |
20 |
21 |
{song?.title}
22 |
23 |
24 |
{song?.subtitle}
25 |
26 |
27 |
28 |
35 |
36 | )
37 |
38 | const TopPlay = () => {
39 | const dispatch = useDispatch();
40 | const { activeSong, isPlaying } = useSelector((state) => state.player);
41 | const { data } = useGetTopChartsQuery();
42 | const divRef = useRef(null);
43 |
44 | useEffect(() => {
45 | divRef.current.scrollIntoView({ behavior: 'smooth' });
46 | });
47 |
48 | const topPlays = data?.slice(0, 5);
49 |
50 | const handlePauseClick = () => {
51 | dispatch(playPause(false));
52 | };
53 |
54 | const handlePlayClick = (song, i) => {
55 | dispatch(setActiveSong({ song, data, i}));
56 | dispatch(playPause(true));
57 | };
58 |
59 | return (
60 |
61 |
62 |
63 |
Top Charts
64 |
65 |
See more
66 |
67 |
68 |
69 | {topPlays?.map((song, i) => (
70 | handlePlayClick(song, i)}
78 | />
79 | ))}
80 |
81 |
82 |
83 |
84 |
85 |
Top Artists
86 |
87 |
See more
88 |
89 |
90 |
91 |
100 | {topPlays?.map((song, i) => {
101 | if (!song || !song.artists) {
102 | return null;
103 | }
104 |
105 | return (
106 |
111 |
112 |
116 |
117 |
118 | );
119 | })}
120 |
121 |
122 |
123 | );
124 |
125 | };
126 |
127 | export default TopPlay;
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Music App Clone (Modified Lyrics Version)
2 |
3 | Develop an elegant React.js Music Application based on the original Project Lyrics by the JavaScript Mastery team. Original README [here](https://github.com/adrianhajdin/project_music_player)
4 |
5 | Check out the complete project requirements [here](https://docs.google.com/document/d/13PeFwRlPEhMw_HPyrIrInvQuKaVWnpNmcv-y3NA208s/edit?usp=sharing)
6 |
7 | # Contributing
8 |
9 | When contributing to this repository, please first discuss the change you wish to make via issue.
10 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
11 |
12 |
13 | ## System Requirements
14 |
15 | To get started with development, you need to install few tools
16 |
17 | 1. git
18 |
19 | `git` version 2.13.1 or higher. Download [git](https://git-scm.com/downloads) if you don't have it already.
20 |
21 | To check your version of git, run:
22 |
23 | ```shell
24 | git --version
25 | ```
26 |
27 | 2. node
28 |
29 | `node` version 16.15.1 or higher. Download [node](https://nodejs.org/en/download/) if you don't have it already.
30 |
31 | To check your version of node, run:
32 |
33 | ```shell
34 | node --version
35 | ```
36 |
37 | 3. npm
38 |
39 | `npm` version 5.6.1 or higher. You will have it after you install node.
40 |
41 | To check your version of npm, run:
42 |
43 | ```shell
44 | npm --version
45 | ```
46 |
47 | ## Setup
48 |
49 | To set up a development environment, please follow these steps:
50 |
51 | 1. Clone the repo
52 |
53 | ```shell
54 | git clone https://github.com/JavaScript-Mastery-PRO/project1_team4_repository.git
55 | ```
56 |
57 | 2. Change directory to the project directory
58 |
59 | ```shell
60 | cd project1_team4_repository
61 | ```
62 |
63 | 3. Install the dependencies
64 |
65 | ```shell
66 | npm install
67 | ```
68 |
69 | If you get an error, please check the console for more information.
70 |
71 | If you don't get an error, you are ready to start development.
72 |
73 | 4. Run the app
74 |
75 | ```shell
76 | npm run dev
77 | ```
78 |
79 | Project will be running in the browser.
80 |
81 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
82 |
83 | ## Issues
84 |
85 | You've found a bug in the source code, a mistake in the documentation or maybe you'd like a new feature? You can help us by [submitting an issue on GitHub](https://github.com/orgs/JavaScript-Mastery-PRO/projects/8). Before you create an issue, make sure to search the issue archive -- your issue may have already been addressed!
86 |
87 | Please try to create bug reports that are:
88 |
89 | - _Reproducible._ Include steps to reproduce the problem.
90 | - _Specific._ Include as much detail as possible: which version, what environment, etc.
91 | - _Unique._ Do not duplicate existing opened issues.
92 | - _Scoped to a Single Bug._ One bug per report.
93 |
94 |
95 | ## Pull Request
96 |
97 | There are 2 main work flows when dealing with pull requests:
98 |
99 | * Pull Request from a [forked repository](https://help.github.com/articles/fork-a-repo)
100 | * Pull Request from a branch within a repository
101 |
102 | Here we are going to focus on 2. Creating a Topical Branch:
103 |
104 |
105 | 1. First, we will need to create a branch from the latest commit on master. Make sure your repository is up to date first using
106 |
107 | ```bash
108 | git pull origin main
109 | ```
110 |
111 | *Note:* `git pull` does a `git fetch` followed by a `git merge` to update the local repo with the remote repo. For a more detailed explanation, see [this stackoverflow post](http://stackoverflow.com/questions/292357/whats-the-difference-between-git-pull-and-git-fetch).
112 |
113 | 2. To create a branch, use `git checkout -b []`, where `base-branch-name` is optional and defaults to `main`.
114 |
115 | Use a standard convention for branch names. For example, `-dev`. It will be easier to track your pull requests if you use this convention.
116 |
117 | I'm going to create a new branch called `jsm-dev` from the `main` branch and push it to github.
118 |
119 | ```bash
120 | git checkout -b jsm-dev main
121 | git push origin jsm-dev
122 | ```
123 |
124 | 3. To create a pull request, you must have changes committed to your new branch.
125 |
126 | 4. Go to [Pull Requests](https://github.com/JavaScript-Mastery-PRO/project1_team4_repository/pulls) and click on the `New Pull Request` button.
127 |
128 | 5. Select the `main` branch as the `base` branch and the `jsm-dev` branch as the `compare` branch.
129 |
130 | 6. Follow the template and fill in the proper information for the pull request.
131 |
132 | 7. Click on the `Submit` button.
133 |
134 | 8. You have successfully created a pull request. Now wait for mentor approval. Once approved, you can merge the pull request.
135 |
136 | #
137 |
--------------------------------------------------------------------------------
/src/assets/loader.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/assets/macFavicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/assets/macLogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------