├── sc.png
├── client
├── vercel.json
├── public
│ ├── cover.jpg
│ ├── cover.png
│ ├── cat_chill.png
│ ├── cat_pop.png
│ ├── cat_rock.png
│ ├── favicon.ico
│ ├── cat_hiphop.png
│ ├── cat_podcast.png
│ ├── cat_romance.png
│ ├── gradient_bg.jpg
│ ├── decided_cover.jpg
│ └── vite.svg
├── src
│ ├── api
│ │ └── index.js
│ ├── theme
│ │ ├── motionVariants.js
│ │ └── index.js
│ ├── utils
│ │ └── index.js
│ ├── pages
│ │ ├── ErrorPage.jsx
│ │ ├── HomePage.jsx
│ │ ├── LibraryPage.jsx
│ │ ├── ArtistesPage.jsx
│ │ ├── PlaylistsPage.jsx
│ │ ├── FavoritesPage.jsx
│ │ ├── ArtistePage.jsx
│ │ ├── LoginPage.jsx
│ │ ├── RegisterPage.jsx
│ │ ├── PlaylistPage.jsx
│ │ ├── EditPlaylistPage.jsx
│ │ └── CreatePlaylistPage.jsx
│ ├── redux
│ │ ├── slices
│ │ │ ├── modalSlice.js
│ │ │ ├── userSlice.js
│ │ │ └── playerSlice.js
│ │ └── store.js
│ ├── components
│ │ ├── Search.jsx
│ │ ├── MusicPlayer
│ │ │ ├── TrackDetails.jsx
│ │ │ ├── VolumeControl.jsx
│ │ │ ├── PlayingBar.jsx
│ │ │ ├── PlayControls.jsx
│ │ │ └── index.jsx
│ │ ├── ArtisteCard.jsx
│ │ ├── PlaylistSong.jsx
│ │ ├── LoadingSkeleton.jsx
│ │ ├── PlaylistCard.jsx
│ │ ├── HomeHero.jsx
│ │ ├── LoginModal.jsx
│ │ ├── CreatePlaylistCard.jsx
│ │ ├── Categories.jsx
│ │ ├── Artistes.jsx
│ │ ├── HorizontalMusicCard.jsx
│ │ ├── SmallSection.jsx
│ │ ├── TopCharts.jsx
│ │ ├── ArtisteSong.jsx
│ │ ├── SongCard.jsx
│ │ └── Navbar.jsx
│ ├── layouts
│ │ ├── HomeLayout.jsx
│ │ └── AuthLayout.jsx
│ ├── main.jsx
│ ├── index.css
│ ├── router
│ │ └── index.jsx
│ └── assets
│ │ └── react.svg
├── vite.config.js
├── .eslintrc.cjs
├── index.html
└── package.json
├── .gitignore
├── server
├── vercel.json
├── config
│ └── dbConnection.js
├── routes
│ ├── artisteRoutes.js
│ ├── userRoutes.js
│ ├── playlistRoutes.js
│ └── songRoutes.js
├── models
│ ├── Artiste.js
│ ├── User.js
│ ├── Playlist.js
│ └── Song.js
├── package.json
├── middleware
│ └── validateToken.js
├── index.js
└── controllers
│ ├── artistController.js
│ ├── userController.js
│ ├── songController.js
│ └── playlistController.js
└── README.md
/sc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/sc.png
--------------------------------------------------------------------------------
/client/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }]
3 | }
4 |
--------------------------------------------------------------------------------
/client/public/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/cover.jpg
--------------------------------------------------------------------------------
/client/public/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/cover.png
--------------------------------------------------------------------------------
/client/public/cat_chill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/cat_chill.png
--------------------------------------------------------------------------------
/client/public/cat_pop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/cat_pop.png
--------------------------------------------------------------------------------
/client/public/cat_rock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/cat_rock.png
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/cat_hiphop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/cat_hiphop.png
--------------------------------------------------------------------------------
/client/public/cat_podcast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/cat_podcast.png
--------------------------------------------------------------------------------
/client/public/cat_romance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/cat_romance.png
--------------------------------------------------------------------------------
/client/public/gradient_bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/gradient_bg.jpg
--------------------------------------------------------------------------------
/client/public/decided_cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ikennaezef/beatbox_music_app/HEAD/client/public/decided_cover.jpg
--------------------------------------------------------------------------------
/client/src/api/index.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | export const client = axios.create({
4 | baseURL: "https://beatbox-music-backend.vercel.app/api/",
5 | });
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | .env
4 |
5 |
6 | .vscode/*
7 | !.vscode/extensions.json
8 | .idea
9 | .DS_Store
10 | *.suo
11 | *.ntvs*
12 | *.njsproj
13 | *.sln
14 | *.sw?
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/server/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [
4 | {
5 | "src": "*.js",
6 | "use": "@vercel/node"
7 | }
8 | ],
9 | "routes": [
10 | {
11 | "src": "/(.*)",
12 | "dest": "/"
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/server/config/dbConnection.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | export const connectDb = async () => {
4 | try {
5 | const connect = await mongoose.connect(process.env.MONGO_CONNECTION_STRING);
6 | console.log("DATABASE CONNECTED", connect.connection.name);
7 | } catch (error) {
8 | console.log(error);
9 | process.exit(1);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/server/routes/artisteRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getArtiste,
4 | getArtistes,
5 | getTopArtistes,
6 | } from "../controllers/artistController.js";
7 |
8 | const router = express.Router();
9 |
10 | router.get("/all", getArtistes);
11 | router.get("/top", getTopArtistes);
12 | router.get("/:id", getArtiste);
13 |
14 | export { router as artisteRouter };
15 |
--------------------------------------------------------------------------------
/client/src/theme/motionVariants.js:
--------------------------------------------------------------------------------
1 | export const appear = {
2 | initial: {
3 | opacity: 0,
4 | },
5 | animate: {
6 | opacity: 1,
7 | transition: { duration: 1, delay: 0.5 },
8 | },
9 | };
10 |
11 | export const fadeInUp = {
12 | initial: {
13 | y: 30,
14 | opacity: 0,
15 | },
16 | animate: {
17 | y: 0,
18 | opacity: 1,
19 | transition: { duration: 1, type: "easeOut" },
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/client/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export const convertToMins = (value) => {
2 | const mins = Math.floor(value / 60);
3 | const secs = Math.round(value - mins * 60, 2);
4 | const formattedSeconds = secs < 10 ? "0" + secs : secs;
5 | return `${mins}:${formattedSeconds}`;
6 | };
7 |
8 | export const truncateText = (text, length) => {
9 | if (text.length > length) {
10 | return text.slice(0, length) + "...";
11 | } else return text;
12 | };
13 |
--------------------------------------------------------------------------------
/client/src/pages/ErrorPage.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, Heading, Text } from "@chakra-ui/react";
2 |
3 | const ErrorPage = () => {
4 | return (
5 |
6 |
7 | 404
8 | An error occured. Page Not Found!
9 |
10 |
11 | );
12 | };
13 |
14 | export default ErrorPage;
15 |
--------------------------------------------------------------------------------
/server/models/Artiste.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const ArtisteSchema = new mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: true,
7 | },
8 | image: {
9 | type: String,
10 | required: true,
11 | },
12 | type: {
13 | type: String,
14 | require: true,
15 | default: "Artiste",
16 | },
17 | bio: {
18 | type: String,
19 | },
20 | });
21 |
22 | const Artiste = mongoose.model("Artiste", ArtisteSchema);
23 | export default Artiste;
24 |
--------------------------------------------------------------------------------
/server/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getUserFavoriteSongs,
4 | loginUser,
5 | registerUser,
6 | } from "../controllers/userController.js";
7 | import { verifyToken } from "../middleware/validateToken.js";
8 |
9 | const router = express.Router();
10 |
11 | router.post("/login", loginUser);
12 | router.post("/register", registerUser);
13 | router.get("/favorites", verifyToken, getUserFavoriteSongs);
14 |
15 | export { router as userRouter };
16 |
--------------------------------------------------------------------------------
/server/models/User.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const UserSchema = new mongoose.Schema({
4 | username: {
5 | type: String,
6 | required: true,
7 | unique: true,
8 | },
9 | password: {
10 | type: String,
11 | required: true,
12 | },
13 | favorites: {
14 | type: Array,
15 | default: [],
16 | },
17 | playlists: {
18 | type: Array,
19 | default: [],
20 | },
21 | });
22 |
23 | const User = mongoose.model("User", UserSchema);
24 | export default User;
25 |
--------------------------------------------------------------------------------
/client/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true },
3 | extends: [
4 | "eslint:recommended",
5 | "plugin:react/recommended",
6 | "plugin:react/jsx-runtime",
7 | "plugin:react-hooks/recommended",
8 | ],
9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
10 | settings: { react: { version: "18.2" } },
11 | plugins: ["react-refresh"],
12 | rules: {
13 | "react-refresh/only-export-components": "warn",
14 | "react/prop-types": "off",
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/redux/slices/modalSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | message: "Please login to save songs to your favorites.",
5 | };
6 |
7 | export const modalSlice = createSlice({
8 | name: "modal",
9 | initialState,
10 | reducers: {
11 | setModalMessage: (state, action) => {
12 | state.message = action.payload;
13 | },
14 | },
15 | });
16 |
17 | export const { setModalMessage } = modalSlice.actions;
18 |
19 | export default modalSlice.reducer;
20 |
--------------------------------------------------------------------------------
/server/routes/playlistRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | createPlaylist,
4 | editPlaylist,
5 | getPlaylist,
6 | getPlaylists,
7 | } from "../controllers/playlistController.js";
8 | import { verifyToken } from "../middleware/validateToken.js";
9 |
10 | const router = express.Router();
11 |
12 | router.get("/", getPlaylists);
13 | router.get("/:id", getPlaylist);
14 | router.post("/create", verifyToken, createPlaylist);
15 | router.patch("/:id", verifyToken, editPlaylist);
16 |
17 | export { router as playlistRouter };
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BeatBox Music 🎵
2 |
3 | ### ✨ [Live Demo](https://beatbox-music.vercel.app)
4 |
5 | ## Overview
6 |
7 | BeatBox is a music app where users can listen to music, save music to their accounts and also create awesome playlists.
8 |
9 | ## Technologies Used
10 |
11 | Built on the MERN Stack with `NodeJs` `ReactJs` `Express` `Chakra UI` `Redux Toolkit` and `MongoDB` for Database Management and storage.
12 |
13 | ## Author
14 |
15 | 👤 **Ikenna Eze**
16 | Leave a ⭐️ If you like this project!
17 |
18 | - Website: https://ikennaezef.netlify.app
19 |
20 | ## Screenshot
21 |
22 | 
23 |
--------------------------------------------------------------------------------
/server/routes/songRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getAroundYou,
4 | getNewReleases,
5 | getRandom,
6 | getSongs,
7 | getTopSongs,
8 | likeSong,
9 | } from "../controllers/songController.js";
10 | import { verifyToken } from "../middleware/validateToken.js";
11 |
12 | const router = express.Router();
13 |
14 | router.get("/", getSongs);
15 | router.get("/top", getTopSongs);
16 | router.get("/releases", getNewReleases);
17 | router.get("/random", getRandom);
18 | router.get("/popular", getAroundYou);
19 | router.patch("/like/:id", verifyToken, likeSong);
20 |
21 | export { router as songsRouter };
22 |
--------------------------------------------------------------------------------
/client/src/theme/index.js:
--------------------------------------------------------------------------------
1 | import { extendTheme } from "@chakra-ui/react";
2 |
3 | const colors = {
4 | zinc: {
5 | 100: "#f4f4f5",
6 | 200: "#e4e4e7",
7 | 300: "#d4d4d8",
8 | 400: "#a1a1aa",
9 | 500: "#71717a",
10 | 600: "#52525b",
11 | 700: "#3f3f46",
12 | 800: "#27272a",
13 | 900: "#18181b",
14 | 950: "#09090b",
15 | },
16 | accent: {
17 | main: "#EE4950",
18 | light: "#ef6067",
19 | transparent: "#f77e8464",
20 | },
21 | };
22 |
23 | const fonts = {
24 | heading: `'Inter', sans-serif`,
25 | body: `'Inter', sans-serif`,
26 | };
27 |
28 | const theme = extendTheme({ colors, fonts });
29 |
30 | export default theme;
31 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "Backend for BeatBox Music",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "dev": "nodemon index.js",
10 | "start": "node index.js"
11 | },
12 | "keywords": [],
13 | "author": "Ikenna Eze",
14 | "license": "ISC",
15 | "dependencies": {
16 | "bcrypt": "^5.1.0",
17 | "cors": "^2.8.5",
18 | "dotenv": "^16.0.3",
19 | "express": "^4.18.2",
20 | "jsonwebtoken": "^9.0.0",
21 | "mongoose": "^7.2.0"
22 | },
23 | "devDependencies": {
24 | "nodemon": "^2.0.22"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/client/src/components/Search.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Input, InputGroup, InputRightElement } from "@chakra-ui/react";
2 | import { BsSearch } from "react-icons/bs";
3 |
4 | const Search = () => {
5 | return (
6 |
7 |
8 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default Search;
26 |
--------------------------------------------------------------------------------
/client/src/redux/slices/userSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | user: null,
5 | token: null,
6 | };
7 |
8 | export const userSlice = createSlice({
9 | name: "user",
10 | initialState,
11 | reducers: {
12 | loginUser: (state, action) => {
13 | state.user = action.payload.user;
14 | state.token = action.payload.token;
15 | },
16 |
17 | logoutUser: (state) => {
18 | state.user = null;
19 | state.token = null;
20 | },
21 | setUser: (state, action) => {
22 | state.user = action.payload;
23 | },
24 | },
25 | });
26 |
27 | export const { loginUser, logoutUser, setUser } = userSlice.actions;
28 |
29 | export default userSlice.reducer;
30 |
--------------------------------------------------------------------------------
/server/models/Playlist.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const PlaylistSchema = new mongoose.Schema(
4 | {
5 | title: {
6 | type: String,
7 | required: true,
8 | },
9 | description: {
10 | type: String,
11 | },
12 | userId: {
13 | type: String,
14 | required: true,
15 | },
16 | userName: {
17 | type: String,
18 | required: true,
19 | },
20 | songs: {
21 | type: Array,
22 | default: [],
23 | },
24 | isPrivate: {
25 | type: Boolean,
26 | required: true,
27 | default: false,
28 | },
29 | type: {
30 | type: String,
31 | required: true,
32 | default: "Playlist",
33 | },
34 | },
35 | { timestamps: true }
36 | );
37 |
38 | const Playlist = mongoose.model("Playlist", PlaylistSchema);
39 | export default Playlist;
40 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 | BeatBox Music
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/server/middleware/validateToken.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | export const verifyToken = async (req, res, next) => {
4 | try {
5 | let token = req.header("Authorization") || req.header("authorization");
6 |
7 | if (!token) {
8 | return res.status(403).send("Authorization missing!");
9 | }
10 |
11 | if (token && token.startsWith("Bearer")) {
12 | token = token.split(" ")[1];
13 |
14 | jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
15 | if (err) {
16 | return res.status(401).send("Invalid auth token");
17 | }
18 | req.user = decoded.user;
19 | next();
20 | });
21 |
22 | if (!token) {
23 | res.status(401).send("Authorization token missing");
24 | }
25 | }
26 | } catch (error) {
27 | res.status(500).json({ message: error.message });
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import dotenv from "dotenv";
3 | import cors from "cors";
4 | import { connectDb } from "./config/dbConnection.js";
5 | import { songsRouter } from "./routes/songRoutes.js";
6 | import { userRouter } from "./routes/userRoutes.js";
7 | import { artisteRouter } from "./routes/artisteRoutes.js";
8 | import { playlistRouter } from "./routes/playlistRoutes.js";
9 |
10 | dotenv.config();
11 |
12 | const app = express();
13 | app.use(express.json());
14 | app.use(cors());
15 |
16 | connectDb();
17 |
18 | app.use("/api/songs/", songsRouter);
19 | app.use("/api/users/", userRouter);
20 | app.use("/api/artistes/", artisteRouter);
21 | app.use("/api/playlists/", playlistRouter);
22 |
23 | const port = process.env.PORT || 6000;
24 |
25 | app.listen(port, async () => {
26 | console.log(`SERVER RUNNING ON PORT ${port}`);
27 | });
28 |
--------------------------------------------------------------------------------
/client/src/components/MusicPlayer/TrackDetails.jsx:
--------------------------------------------------------------------------------
1 | import { Flex, Image, Text } from "@chakra-ui/react";
2 |
3 | const TrackDetails = ({ track }) => {
4 | return (
5 |
6 |
14 |
15 |
19 | {track?.title}
20 |
21 |
26 | {track?.artistes.join(", ")}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default TrackDetails;
34 |
--------------------------------------------------------------------------------
/server/models/Song.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const SongSchema = new mongoose.Schema({
4 | title: {
5 | type: String,
6 | required: true,
7 | },
8 | duration: {
9 | type: String,
10 | required: true,
11 | },
12 | coverImage: {
13 | type: String,
14 | required: true,
15 | default:
16 | "https://firebasestorage.googleapis.com/v0/b/socialstream-ba300.appspot.com/o/music_app_files%2Fplaylist_cover.jpg?alt=media&token=546adcad-e9c3-402f-8a57-b7ba252100ec",
17 | },
18 | artistes: {
19 | type: Array,
20 | default: [],
21 | },
22 | artistIds: [
23 | {
24 | type: mongoose.Schema.Types.ObjectId,
25 | ref: "Artiste",
26 | },
27 | ],
28 | likes: {
29 | type: Map,
30 | of: Boolean,
31 | },
32 | songUrl: {
33 | type: String,
34 | required: true,
35 | },
36 | type: {
37 | type: String,
38 | required: true,
39 | default: "Song",
40 | },
41 | });
42 |
43 | const Song = mongoose.model("Song", SongSchema);
44 | export default Song;
45 |
--------------------------------------------------------------------------------
/client/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, configureStore } from "@reduxjs/toolkit";
2 | import {
3 | persistReducer,
4 | FLUSH,
5 | PAUSE,
6 | PERSIST,
7 | PURGE,
8 | REGISTER,
9 | REHYDRATE,
10 | } from "redux-persist";
11 | import storage from "redux-persist/lib/storage";
12 | import playerReducer from "./slices/playerSlice";
13 | import userReducer from "./slices/userSlice";
14 | import modalReducer from "./slices/modalSlice";
15 |
16 | const persistConfig = { key: "root", storage, version: 1 };
17 | const reducers = combineReducers({
18 | player: playerReducer,
19 | user: userReducer,
20 | modal: modalReducer,
21 | });
22 | const persistedReducers = persistReducer(persistConfig, reducers);
23 | export const store = configureStore({
24 | reducer: persistedReducers,
25 | middleware: (getDefaultMiddleware) =>
26 | getDefaultMiddleware({
27 | serializableCheck: {
28 | ignoreActions: [FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE],
29 | },
30 | }),
31 | });
32 |
--------------------------------------------------------------------------------
/client/src/components/MusicPlayer/VolumeControl.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Flex,
4 | Slider,
5 | SliderFilledTrack,
6 | SliderThumb,
7 | SliderTrack,
8 | } from "@chakra-ui/react";
9 | import { BsFillVolumeMuteFill, BsFillVolumeUpFill } from "react-icons/bs";
10 |
11 | const VolumeControl = ({ onToggle, onChange, volume }) => {
12 | return (
13 |
14 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default VolumeControl;
39 |
--------------------------------------------------------------------------------
/client/src/layouts/HomeLayout.jsx:
--------------------------------------------------------------------------------
1 | import Navbar from "../components/Navbar";
2 | import { Outlet, useLocation } from "react-router-dom";
3 | import { Grid, GridItem } from "@chakra-ui/react";
4 | import { MusicPlayer } from "../components/MusicPlayer/index.jsx";
5 | import { useSelector } from "react-redux";
6 | import { useEffect } from "react";
7 |
8 | const HomeLayout = () => {
9 | const { currentTrack } = useSelector((state) => state.player);
10 | const { pathname } = useLocation();
11 |
12 | useEffect(() => {
13 | window.scrollTo(0, 0);
14 | }, [pathname]);
15 |
16 | return (
17 | <>
18 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {currentTrack && }
31 |
32 | >
33 | );
34 | };
35 |
36 | export default HomeLayout;
37 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import { router } from "./router";
5 | import { Provider } from "react-redux";
6 | import { RouterProvider } from "react-router-dom";
7 | import { ChakraProvider } from "@chakra-ui/react";
8 | import theme from "./theme/index.js";
9 | import { store } from "./redux/store.js";
10 | import { PersistGate } from "redux-persist/integration/react";
11 | import { persistStore } from "redux-persist";
12 |
13 | ReactDOM.createRoot(document.getElementById("root")).render(
14 |
15 |
16 |
17 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 |
--------------------------------------------------------------------------------
/client/src/pages/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import HomeHero from "../components/HomeHero";
2 | import SmallSection from "../components/SmallSection";
3 | import TopCharts from "../components/TopCharts";
4 | import Categories from "../components/Categories";
5 | import Search from "../components/Search";
6 | import { Grid, GridItem, Hide } from "@chakra-ui/react";
7 | import Artistes from "../components/Artistes";
8 |
9 | const HomePage = () => {
10 | return (
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default HomePage;
35 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "music_app",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "author": "Ikenna Eze",
13 | "license": "ISC",
14 | "dependencies": {
15 | "@chakra-ui/react": "^2.6.1",
16 | "@emotion/react": "^11.11.0",
17 | "@emotion/styled": "^11.11.0",
18 | "@reduxjs/toolkit": "^1.9.5",
19 | "axios": "^1.4.0",
20 | "framer-motion": "^10.12.14",
21 | "react": "^18.2.0",
22 | "react-dom": "^18.2.0",
23 | "react-icons": "^4.8.0",
24 | "react-redux": "^8.0.5",
25 | "react-router-dom": "^6.11.2",
26 | "redux-persist": "^6.0.0"
27 | },
28 | "devDependencies": {
29 | "@types/react": "^18.0.28",
30 | "@types/react-dom": "^18.0.11",
31 | "@vitejs/plugin-react": "^4.0.0",
32 | "eslint": "^8.38.0",
33 | "eslint-plugin-react": "^7.32.2",
34 | "eslint-plugin-react-hooks": "^4.6.0",
35 | "eslint-plugin-react-refresh": "^0.3.4",
36 | "vite": "^4.3.2"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/client/src/components/ArtisteCard.jsx:
--------------------------------------------------------------------------------
1 | import { Flex, Image, Text } from "@chakra-ui/react";
2 | import { Link } from "react-router-dom";
3 |
4 | const ArtisteCard = ({ artiste }) => {
5 | return (
6 |
7 |
13 |
20 |
29 |
30 |
35 | {artiste?.name}
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default ArtisteCard;
43 |
--------------------------------------------------------------------------------
/client/src/components/PlaylistSong.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Flex, Image, Text } from "@chakra-ui/react";
2 | import { BiPlusCircle } from "react-icons/bi";
3 | import { BsCheckCircle } from "react-icons/bs";
4 |
5 | const PlaylistSong = ({ song, isAdded, onToggleAdd }) => {
6 | return (
7 |
13 |
14 |
20 |
21 | {song?.title}
22 |
23 | {song?.artistes.join(", ")}
24 |
25 |
26 |
27 |
37 |
38 | );
39 | };
40 |
41 | export default PlaylistSong;
42 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap");
2 |
3 | body,
4 | html {
5 | font-family: "Inter", sans-serif;
6 | /* background-color: #212121; */
7 | color: #e3e3e3;
8 | position: relative;
9 | }
10 |
11 | h1,
12 | h2,
13 | h3,
14 | h4,
15 | h5,
16 | h6,
17 | p,
18 | span {
19 | color: #e3e3e3;
20 | }
21 |
22 | body::-webkit-scrollbar {
23 | width: 3px;
24 | height: 3px;
25 | background: #333;
26 | margin: 1rem;
27 | }
28 |
29 | body::-webkit-scrollbar-thumb {
30 | width: 5px;
31 | border-radius: 5px;
32 | background: #636262;
33 | margin: 1rem;
34 | }
35 |
36 | .scrollbar_style::-webkit-scrollbar {
37 | display: none;
38 | width: 3px;
39 | height: 3px;
40 | background: transparent;
41 | margin: 1rem;
42 | }
43 |
44 | .scrollbar_style::-webkit-scrollbar-thumb {
45 | width: 5px;
46 | border-radius: 5px;
47 | background: #3a3939;
48 | margin: 1rem;
49 | }
50 |
51 | .spin {
52 | animation: spin 1s linear infinite;
53 | }
54 |
55 | @keyframes spin {
56 | from {
57 | transform: rotate(0deg);
58 | }
59 | to {
60 | transform: rotate(360deg);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/client/src/layouts/AuthLayout.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, Heading } from "@chakra-ui/react";
2 | import { useEffect } from "react";
3 | import { BiMusic } from "react-icons/bi";
4 | import { useSelector } from "react-redux";
5 | import { Outlet, useNavigate } from "react-router-dom";
6 |
7 | const AuthLayout = () => {
8 | const { user } = useSelector((state) => state.user);
9 | const navigate = useNavigate();
10 |
11 | useEffect(() => {
12 | if (user) {
13 | navigate("/home");
14 | }
15 | }, [user]);
16 | return (
17 |
18 |
25 |
26 |
27 |
31 | BeatBox
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default AuthLayout;
43 |
--------------------------------------------------------------------------------
/client/src/components/MusicPlayer/PlayingBar.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Flex,
4 | Slider,
5 | SliderFilledTrack,
6 | SliderThumb,
7 | SliderTrack,
8 | Text,
9 | } from "@chakra-ui/react";
10 | import { BsSoundwave } from "react-icons/bs";
11 | import { convertToMins } from "../../utils";
12 |
13 | const PlayingBar = ({ time, track, onSeek, trackRef }) => {
14 | return (
15 |
16 |
17 | {trackRef ? convertToMins(trackRef.currentTime) : "0:00"}
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {track?.duration.split(".").join(":")}
36 |
37 |
38 | );
39 | };
40 |
41 | export default PlayingBar;
42 |
--------------------------------------------------------------------------------
/client/src/components/LoadingSkeleton.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, Skeleton, SkeletonText } from "@chakra-ui/react";
2 |
3 | const LoadingSkeleton = () => {
4 | return (
5 |
6 |
7 |
13 |
14 |
22 |
30 |
31 |
32 |
33 |
40 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default LoadingSkeleton;
54 |
--------------------------------------------------------------------------------
/client/src/components/PlaylistCard.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Heading, Image, Text } from "@chakra-ui/react";
2 | import { motion } from "framer-motion";
3 | import { Link } from "react-router-dom";
4 | import { fadeInUp } from "../theme/motionVariants";
5 |
6 | const PlaylistCard = ({ playlist }) => {
7 | return (
8 |
9 |
20 |
30 |
31 |
36 | {playlist?.title}
37 |
38 |
42 | {playlist?.userName}
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default PlaylistCard;
51 |
--------------------------------------------------------------------------------
/client/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/controllers/artistController.js:
--------------------------------------------------------------------------------
1 | import Artiste from "../models/Artiste.js";
2 | import Song from "../models/Song.js";
3 |
4 | //@desc Get all the artistes
5 | //@route GET /api/artistes/all
6 | //@access public
7 | const getArtistes = async (req, res) => {
8 | const artistes = await Artiste.find();
9 |
10 | if (!artistes) {
11 | res.status(400).json({ message: "Artistes not found!" });
12 | }
13 |
14 | res.status(200).json(artistes);
15 | };
16 |
17 | //@desc Get the top artistes
18 | //@route GET /api/artistes/top
19 | //@access public
20 | const getTopArtistes = async (req, res) => {
21 | const artistes = await Artiste.find();
22 |
23 | if (!artistes) {
24 | res.status(400).json({ message: "Artistes not found!" });
25 | }
26 |
27 | const result = artistes.slice(1, 11);
28 |
29 | res.status(200).json(result);
30 | };
31 |
32 | //@desc Get details for an artiste
33 | //@route GET /api/artistes/:id
34 | //@access public
35 | const getArtiste = async (req, res) => {
36 | const { id } = req.params;
37 |
38 | const artiste = await Artiste.findById(id);
39 | if (!artiste) {
40 | return res.status(404).json({ message: "Artiste not found!" });
41 | }
42 |
43 | const artisteSongs = await Song.find({ artistIds: id });
44 | if (!artisteSongs) {
45 | return res.status(400).json({ message: "An error occured!" });
46 | }
47 |
48 | res.status(200).json({ ...artiste._doc, songs: artisteSongs });
49 | };
50 |
51 | export { getArtiste, getArtistes, getTopArtistes };
52 |
--------------------------------------------------------------------------------
/client/src/components/HomeHero.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Flex, Heading, Text } from "@chakra-ui/react";
2 | import { motion } from "framer-motion";
3 | import { appear } from "../theme/motionVariants";
4 | import { Link } from "react-router-dom";
5 |
6 | const HomeHero = () => {
7 | return (
8 |
18 |
28 |
29 |
34 | Amazing Playlists
35 |
36 |
37 | Listen to the best playlists curated by us and our users.
38 |
39 |
40 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default HomeHero;
57 |
--------------------------------------------------------------------------------
/client/src/components/LoginModal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | AlertDialog,
4 | AlertDialogBody,
5 | AlertDialogCloseButton,
6 | AlertDialogContent,
7 | AlertDialogFooter,
8 | AlertDialogHeader,
9 | AlertDialogOverlay,
10 | Button,
11 | } from "@chakra-ui/react";
12 | import { useSelector } from "react-redux";
13 | import { useNavigate } from "react-router-dom";
14 |
15 | const LoginModal = React.forwardRef((props, ref) => {
16 | const navigate = useNavigate();
17 | const { message } = useSelector((state) => state.modal);
18 | return (
19 |
25 |
26 |
27 |
28 |
29 | Not Logged In
30 |
31 |
32 |
33 | {message}
34 |
35 |
36 |
42 |
49 |
50 |
51 |
52 | );
53 | });
54 |
55 | LoginModal.displayName = "LoginModal";
56 |
57 | export default LoginModal;
58 |
--------------------------------------------------------------------------------
/client/src/components/CreatePlaylistCard.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Flex, Text, useDisclosure } from "@chakra-ui/react";
2 | import { useRef } from "react";
3 | import { BiPlus } from "react-icons/bi";
4 | import LoginModal from "./LoginModal";
5 | import { useDispatch, useSelector } from "react-redux";
6 | import { useNavigate } from "react-router-dom";
7 | import { setModalMessage } from "../redux/slices/modalSlice";
8 |
9 | const CreatePlaylistCard = () => {
10 | const { isOpen, onOpen, onClose } = useDisclosure();
11 | const modalRef = useRef();
12 | const navigate = useNavigate();
13 |
14 | const { user } = useSelector((state) => state.user);
15 | const dispatch = useDispatch();
16 |
17 | const handleCreatePlaylist = () => {
18 | if (user) {
19 | navigate("/playlists/create");
20 | } else {
21 | dispatch(setModalMessage("Please login to create a playlist."));
22 | onOpen();
23 | }
24 | };
25 |
26 | return (
27 | <>
28 |
29 |
36 |
47 |
48 | Create a playlist
49 |
50 |
51 | >
52 | );
53 | };
54 |
55 | export default CreatePlaylistCard;
56 |
--------------------------------------------------------------------------------
/client/src/components/Categories.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex, Heading, Image, SimpleGrid } from "@chakra-ui/react";
2 |
3 | const categories = [
4 | {
5 | id: 1,
6 | title: "Pop",
7 | image: "/cat_pop.png",
8 | },
9 | {
10 | id: 2,
11 | title: "Chill",
12 | image: "/cat_chill.png",
13 | },
14 | {
15 | id: 3,
16 | title: "Podcast",
17 | image: "/cat_podcast.png",
18 | },
19 | {
20 | id: 4,
21 | title: "Romance",
22 | image: "/cat_romance.png",
23 | },
24 | {
25 | id: 5,
26 | title: "Hip Hop",
27 | image: "/cat_hiphop.png",
28 | },
29 | {
30 | id: 6,
31 | title: "Rock",
32 | image: "/cat_rock.png",
33 | },
34 | ];
35 |
36 | const Categories = () => {
37 | return (
38 |
39 |
40 | Categories
41 |
42 |
43 |
44 | {categories.map((cat) => (
45 |
53 |
64 |
72 |
73 | {cat.title}
74 |
75 |
76 |
77 | ))}
78 |
79 |
80 | );
81 | };
82 |
83 | export default Categories;
84 |
--------------------------------------------------------------------------------
/client/src/pages/LibraryPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import SongCard from "../components/SongCard";
3 | import { AiOutlineLoading } from "react-icons/ai";
4 | import { Box, Flex, Grid, Heading, Text } from "@chakra-ui/react";
5 | import { client } from "../api";
6 |
7 | const LibraryPage = () => {
8 | const [songs, setSongs] = useState([]);
9 | const [loading, setLoading] = useState(true);
10 | const [error, setError] = useState(false);
11 |
12 | const fetchSongs = async () => {
13 | setLoading(true);
14 | setError(false);
15 | await client
16 | .get("/songs")
17 | .then((res) => {
18 | setSongs(res.data);
19 | setLoading(false);
20 | })
21 | .catch(() => {
22 | setError(true);
23 | setLoading(false);
24 | });
25 | };
26 |
27 | useEffect(() => {
28 | fetchSongs();
29 | }, []);
30 |
31 | return (
32 |
38 |
39 |
43 | Library
44 |
45 |
46 | Discover interesting songs
47 |
48 |
49 | {loading && songs.length < 1 && (
50 |
51 |
52 |
53 | )}
54 |
62 | {songs.map((song) => (
63 |
64 | ))}
65 |
66 | {error && (
67 |
68 | Sorry, an error occured
69 |
70 | )}
71 |
72 | );
73 | };
74 |
75 | export default LibraryPage;
76 |
--------------------------------------------------------------------------------
/client/src/pages/ArtistesPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import ArtisteCard from "../components/ArtisteCard";
3 | import { AiOutlineLoading } from "react-icons/ai";
4 | import { Box, Flex, Grid, Heading, Text } from "@chakra-ui/react";
5 | import { client } from "../api";
6 |
7 | const ArtistesPage = () => {
8 | const [artistes, setArtistes] = useState([]);
9 | const [loading, setLoading] = useState(true);
10 | const [error, setError] = useState(false);
11 |
12 | const fetchArtistes = async () => {
13 | setLoading(true);
14 | setError(false);
15 | await client
16 | .get("/artistes/all")
17 | .then((res) => {
18 | setArtistes(res.data);
19 | setLoading(false);
20 | })
21 | .catch(() => {
22 | setError(true);
23 | setLoading(false);
24 | });
25 | };
26 |
27 | useEffect(() => {
28 | fetchArtistes();
29 | }, []);
30 |
31 | return (
32 |
38 |
39 |
43 | Artistes
44 |
45 |
46 | Discover new artistes
47 |
48 |
49 | {loading && artistes.length < 1 && (
50 |
51 |
52 |
53 | )}
54 |
62 | {artistes.map((artiste) => (
63 |
64 | ))}
65 |
66 | {error && (
67 |
68 | Sorry, an error occured
69 |
70 | )}
71 |
72 | );
73 | };
74 |
75 | export default ArtistesPage;
76 |
--------------------------------------------------------------------------------
/client/src/components/Artistes.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { Box, Button, Flex, Heading, Text } from "@chakra-ui/react";
3 | import { AiOutlineLoading } from "react-icons/ai";
4 | import ArtisteCard from "./ArtisteCard";
5 | import { client } from "../api";
6 | import { Link } from "react-router-dom";
7 |
8 | const Artistes = () => {
9 | const [artistes, setArtistes] = useState([]);
10 | const [loading, setLoading] = useState(true);
11 | const [error, setError] = useState(false);
12 |
13 | const fetchArtistes = async () => {
14 | setLoading(true);
15 | setError(false);
16 | await client
17 | .get("/artistes/top")
18 | .then((res) => {
19 | setArtistes(res.data);
20 | setLoading(false);
21 | })
22 | .catch(() => {
23 | setError(true);
24 | setLoading(false);
25 | });
26 | };
27 |
28 | useEffect(() => {
29 | fetchArtistes();
30 | }, []);
31 |
32 | return (
33 |
34 |
35 |
36 | You May Like
37 |
38 |
39 |
46 |
47 |
48 |
49 | {loading ? (
50 |
51 |
52 |
53 | ) : error ? (
54 |
55 | Sorry, an error occured
56 |
57 | ) : (
58 |
66 | {artistes?.map((artiste) => (
67 |
68 | ))}
69 |
70 | )}
71 |
72 | );
73 | };
74 |
75 | export default Artistes;
76 |
--------------------------------------------------------------------------------
/client/src/router/index.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, createBrowserRouter } from "react-router-dom";
2 | import HomePage from "../pages/HomePage";
3 | import HomeLayout from "../layouts/HomeLayout";
4 | import LibraryPage from "../pages/LibraryPage";
5 | import PlaylistsPage from "../pages/PlaylistsPage";
6 | import ErrorPage from "../pages/ErrorPage";
7 | import AuthLayout from "../layouts/AuthLayout";
8 | import LoginPage from "../pages/LoginPage";
9 | import RegisterPage from "../pages/RegisterPage";
10 | import ArtistePage from "../pages/ArtistePage";
11 | import ArtistesPage from "../pages/ArtistesPage";
12 | import FavoritesPage from "../pages/FavoritesPage";
13 | import PlaylistPage from "../pages/PlaylistPage";
14 | import CreatePlaylistPage from "../pages/CreatePlaylistPage";
15 | import EditPlaylistPage from "../pages/EditPlaylistPage";
16 |
17 | export const router = createBrowserRouter([
18 | {
19 | path: "/",
20 | element: ,
21 | errorElement: ,
22 | children: [
23 | { index: true, element: },
24 | {
25 | path: "/home",
26 | element: ,
27 | },
28 | {
29 | path: "library",
30 | element: ,
31 | },
32 | {
33 | path: "playlists",
34 | element: ,
35 | },
36 | {
37 | path: "playlists/:id",
38 | element: ,
39 | },
40 | {
41 | path: "playlists/create",
42 | element: ,
43 | },
44 | {
45 | path: "playlists/edit/:id",
46 | element: ,
47 | },
48 | {
49 | path: "artistes",
50 | element: ,
51 | },
52 | {
53 | path: "artiste/:id",
54 | element: ,
55 | },
56 | {
57 | path: "favorites",
58 | element: ,
59 | },
60 | ],
61 | },
62 | {
63 | path: "/auth",
64 | element: ,
65 | errorElement: ,
66 | children: [
67 | {
68 | path: "login",
69 | element: ,
70 | },
71 | {
72 | path: "register",
73 | element: ,
74 | },
75 | ],
76 | },
77 | ]);
78 |
--------------------------------------------------------------------------------
/client/src/components/HorizontalMusicCard.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Flex,
5 | Heading,
6 | Hide,
7 | Image,
8 | Text,
9 | } from "@chakra-ui/react";
10 | import { AiFillHeart, AiOutlineHeart } from "react-icons/ai";
11 | import { motion } from "framer-motion";
12 | import { fadeInUp } from "../theme/motionVariants";
13 | import { useSelector } from "react-redux";
14 |
15 | const HorizontalMusicCard = ({ song, onPlay }) => {
16 | const { currentTrack } = useSelector((state) => state.player);
17 | const { user } = useSelector((state) => state.user);
18 |
19 | return (
20 | onPlay(song)}
23 | as={motion.div}
24 | initial="initial"
25 | animate="animate"
26 | variants={fadeInUp}
27 | bg="zinc.900"
28 | p={2}
29 | w="full"
30 | rounded="base">
31 |
32 |
33 |
41 |
42 |
50 | {song?.title}
51 |
52 |
53 | {song?.artistes?.join(", ")}
54 |
55 |
56 |
57 |
58 |
59 |
60 | {song?.duration?.split(".")?.join(":")}
61 |
62 |
63 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default HorizontalMusicCard;
77 |
--------------------------------------------------------------------------------
/client/src/components/SmallSection.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { AiFillPlayCircle, AiOutlineLoading } from "react-icons/ai";
3 | import SongCard from "./SongCard";
4 | import { Box, Button, Flex, Heading, Text } from "@chakra-ui/react";
5 | import { client } from "../api";
6 | import { Link } from "react-router-dom";
7 |
8 | const SmallSection = ({ title, endpoint }) => {
9 | const [loading, setLoading] = useState(false);
10 | const [data, setData] = useState([]);
11 | const [error, setError] = useState(false);
12 |
13 | const fetchData = async () => {
14 | setError(false);
15 | setLoading(true);
16 | await client
17 | .get(`${endpoint}`)
18 | .then((res) => {
19 | setData(res.data);
20 | setLoading(false);
21 | })
22 | .catch(() => {
23 | setError(true);
24 | setLoading(false);
25 | });
26 | };
27 |
28 | useEffect(() => {
29 | fetchData();
30 | }, []);
31 |
32 | return (
33 |
34 |
35 |
36 |
37 | {title}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
51 |
52 |
53 | {loading ? (
54 |
55 |
56 |
57 | ) : error ? (
58 |
59 | Sorry, an error occured
60 |
61 | ) : (
62 |
69 | {data?.map((song) => (
70 |
71 | ))}
72 |
73 | )}
74 |
75 | );
76 | };
77 |
78 | export default SmallSection;
79 |
--------------------------------------------------------------------------------
/client/src/redux/slices/playerSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | currentTrack: null,
5 | isPlaying: false,
6 | currentIndex: 0,
7 | trackList: [],
8 | repeatStatus: "OFF",
9 | };
10 |
11 | export const playerSlice = createSlice({
12 | name: "player",
13 | initialState,
14 | reducers: {
15 | resetPlayer: (state) => {
16 | state.currentTrack = null;
17 | state.isPlaying = false;
18 | state.currentIndex = 0;
19 | state.trackList = [];
20 | state.repeatStatus = "OFF";
21 | },
22 | setCurrentTrack: (state, action) => {
23 | state.currentTrack = action.payload;
24 | },
25 | setPlaying: (state, action) => {
26 | state.isPlaying = action.payload;
27 | },
28 | playTrack: (state, action) => {
29 | state.currentTrack = action.payload;
30 | state.isPlaying = true;
31 | },
32 | setTrackList: (state, action) => {
33 | state.trackList = action.payload.list;
34 | state.currentIndex = action.payload.index ? action.payload.index : 0;
35 | },
36 | nextTrack: (state) => {
37 | if (state.currentIndex >= state.trackList.length - 1) {
38 | state.currentIndex = 0;
39 | state.currentTrack = state.trackList[0];
40 | } else {
41 | state.currentTrack = state.trackList[state.currentIndex + 1];
42 | state.currentIndex += 1;
43 | }
44 | },
45 | prevTrack: (state) => {
46 | if (state.currentIndex == 0) {
47 | state.currentIndex = state.trackList.length - 1;
48 | state.currentTrack = state.trackList[state.trackList.length - 1];
49 | } else {
50 | state.currentTrack = state.trackList[state.currentIndex - 1];
51 | state.currentIndex -= 1;
52 | }
53 | },
54 | toggleRepeat: (state) => {
55 | switch (state.repeatStatus) {
56 | case "OFF":
57 | state.repeatStatus = "TRACKLIST";
58 | break;
59 | case "TRACKLIST":
60 | state.repeatStatus = "SINGLE";
61 | break;
62 | case "SINGLE":
63 | state.repeatStatus = "OFF";
64 | break;
65 | default:
66 | break;
67 | }
68 | },
69 | },
70 | });
71 |
72 | export const {
73 | resetPlayer,
74 | setCurrentTrack,
75 | setPlaying,
76 | playTrack,
77 | setTrackList,
78 | nextTrack,
79 | prevTrack,
80 | toggleRepeat,
81 | } = playerSlice.actions;
82 |
83 | export default playerSlice.reducer;
84 |
--------------------------------------------------------------------------------
/client/src/components/TopCharts.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import HorizontalMusicCard from "./HorizontalMusicCard";
3 | import { Box, Flex, Heading, Text } from "@chakra-ui/react";
4 | import { motion } from "framer-motion";
5 | import { appear } from "../theme/motionVariants";
6 | import { client } from "../api";
7 | import { AiOutlineLoading } from "react-icons/ai";
8 | import { useDispatch } from "react-redux";
9 | import { playTrack, setTrackList } from "../redux/slices/playerSlice";
10 |
11 | const TopCharts = () => {
12 | const dispatch = useDispatch();
13 | const [loading, setLoading] = useState(true);
14 | const [data, setData] = useState([]);
15 | const [error, setError] = useState(false);
16 |
17 | const fetchData = async () => {
18 | setLoading(true);
19 | setError(false);
20 | await client
21 | .get("/songs/top")
22 | .then((res) => {
23 | setData(res.data);
24 | setLoading(false);
25 | })
26 | .catch(() => {
27 | setError(true);
28 | setLoading(false);
29 | });
30 | };
31 |
32 | useEffect(() => {
33 | fetchData();
34 | }, []);
35 |
36 | const handlePlaySong = (song) => {
37 | const index = data?.findIndex((s) => s._id == song._id);
38 |
39 | dispatch(setTrackList({ list: data, index }));
40 | dispatch(playTrack(song));
41 | };
42 |
43 | return (
44 |
52 |
53 | Top Charts
54 |
55 | {loading ? (
56 |
57 |
58 |
59 | ) : (
60 |
61 | {data?.map((song, i) => (
62 |
63 | {1 + i}
64 |
69 |
70 | ))}
71 |
72 | )}
73 |
74 | {error && (
75 |
76 | An error occured while fetching top charts.
77 |
78 | )}
79 |
80 | );
81 | };
82 |
83 | export default TopCharts;
84 |
--------------------------------------------------------------------------------
/client/src/pages/PlaylistsPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import PlaylistCard from "../components/PlaylistCard";
3 | import { Box, Flex, Grid, Heading, Text } from "@chakra-ui/react";
4 | import CreatePlaylistCard from "../components/CreatePlaylistCard";
5 | import { client } from "../api";
6 | import { AiOutlineLoading } from "react-icons/ai";
7 | import { MdErrorOutline } from "react-icons/md";
8 |
9 | const PlaylistsPage = () => {
10 | const [loading, setLoading] = useState(false);
11 | const [error, setError] = useState(false);
12 | const [playlists, setPlaylists] = useState([]);
13 |
14 | const fetchPlaylists = async () => {
15 | setLoading(true);
16 | setError(false);
17 | await client
18 | .get("/playlists")
19 | .then((res) => {
20 | setLoading(false);
21 | setPlaylists(res.data);
22 | })
23 | .catch(() => {
24 | setLoading(false);
25 | setError(true);
26 | });
27 | };
28 |
29 | useEffect(() => {
30 | fetchPlaylists();
31 | }, []);
32 |
33 | if (error) {
34 | return (
35 |
36 |
37 |
38 |
39 | An error occured while fetching playlists.
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | return (
47 |
48 |
49 |
54 | Playlists
55 |
56 |
57 | Here are some playlists curated by users.
58 |
59 |
60 | {loading && playlists.length < 1 && (
61 |
62 |
63 |
64 | )}
65 | {!loading && !error && (
66 |
75 |
76 | {playlists.map((playlist) => (
77 |
78 | ))}
79 |
80 | )}
81 |
82 | );
83 | };
84 |
85 | export default PlaylistsPage;
86 |
--------------------------------------------------------------------------------
/client/src/components/MusicPlayer/PlayControls.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Flex, Hide } from "@chakra-ui/react";
2 | import { AiFillPauseCircle, AiFillPlayCircle } from "react-icons/ai";
3 | import {
4 | TbArrowsShuffle,
5 | TbPlayerTrackNextFilled,
6 | TbPlayerTrackPrevFilled,
7 | TbRepeat,
8 | TbRepeatOff,
9 | TbRepeatOnce,
10 | } from "react-icons/tb";
11 | import { useDispatch } from "react-redux";
12 | import { toggleRepeat } from "../../redux/slices/playerSlice";
13 |
14 | const PlayControls = ({
15 | onNext,
16 | onPrevious,
17 | onPlay,
18 | isPlaying,
19 | repeatStatus,
20 | }) => {
21 | const dispatch = useDispatch();
22 | return (
23 |
28 |
29 |
37 |
38 |
50 |
61 |
72 |
73 |
88 |
89 |
90 | );
91 | };
92 |
93 | export default PlayControls;
94 |
--------------------------------------------------------------------------------
/server/controllers/userController.js:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcrypt";
2 | import jwt from "jsonwebtoken";
3 | import User from "../models/User.js";
4 | import Song from "../models/Song.js";
5 |
6 | //@desc Login a user
7 | //@route POST /api/auth/login
8 | //@access public
9 | const loginUser = async (req, res) => {
10 | const { username, password } = req.body;
11 |
12 | if (!username || !password) {
13 | return res.status(400).json({ message: "All fields are required!" });
14 | }
15 |
16 | const user = await User.findOne({ username });
17 |
18 | if (!user) {
19 | return res.status(404).json({ message: "User not found!" });
20 | }
21 |
22 | const passwordMatch = await bcrypt.compare(password, user.password);
23 | if (!passwordMatch) {
24 | return res.status(400).json({ message: "Incorrect username or password!" });
25 | }
26 |
27 | const accessToken = jwt.sign(
28 | {
29 | user: {
30 | id: user.id,
31 | username: user.username,
32 | },
33 | },
34 | process.env.JWT_SECRET
35 | );
36 |
37 | const returnedUser = {
38 | id: user.id,
39 | username: user.username,
40 | favorites: user.favorites,
41 | playlists: user.playlists,
42 | };
43 |
44 | res.status(200).json({ user: returnedUser, token: accessToken });
45 | };
46 |
47 | //@desc Login a user
48 | //@route POST /api/auth/register
49 | //@access public
50 | const registerUser = async (req, res) => {
51 | const { username, password } = req.body;
52 |
53 | if (!username || !password) {
54 | return res.status(400).json({ message: "All fields are required" });
55 | }
56 |
57 | const duplicateUsername = await User.findOne({ username });
58 | if (duplicateUsername) {
59 | return res.status(400).json({ message: "Username already exists!" });
60 | }
61 |
62 | const hashedPassword = await bcrypt.hash(password, 10);
63 |
64 | const newUser = await User.create({ username, password: hashedPassword });
65 | if (!newUser) {
66 | return res.status(400).json({ message: "User not created!" });
67 | }
68 |
69 | const accessToken = jwt.sign(
70 | {
71 | user: {
72 | id: newUser.id,
73 | username: newUser.username,
74 | },
75 | },
76 | process.env.JWT_SECRET
77 | );
78 |
79 | const returnedUser = {
80 | id: newUser.id,
81 | username: newUser.username,
82 | favorites: newUser.favorites,
83 | playlists: newUser.playlists,
84 | };
85 |
86 | res.status(200).json({ user: returnedUser, token: accessToken });
87 | };
88 |
89 | //@desc Get a user's favorite songs
90 | //@route GET /api/songs/user/favorites
91 | //@access private
92 | const getUserFavoriteSongs = async (req, res) => {
93 | const { id } = req.user;
94 | const user = await User.findById(id);
95 |
96 | if (!user) {
97 | return res.status(404).json({ message: "User not found!" });
98 | }
99 |
100 | const userFavorites = await Promise.all(
101 | user.favorites.map((id) => Song.findById(id))
102 | );
103 |
104 | if (!userFavorites) {
105 | return res.status(404).json({ message: "Not found!" });
106 | }
107 |
108 | res.status(200).json(userFavorites);
109 | };
110 |
111 | export { loginUser, registerUser, getUserFavoriteSongs };
112 |
--------------------------------------------------------------------------------
/client/src/pages/FavoritesPage.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Divider, Flex, Heading, Text } from "@chakra-ui/react";
2 | import { useEffect, useState } from "react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { Link } from "react-router-dom";
5 | import { client } from "../api";
6 | import ArtisteSong from "../components/ArtisteSong";
7 | import { playTrack, setTrackList } from "../redux/slices/playerSlice";
8 | import { AiOutlineLoading } from "react-icons/ai";
9 |
10 | const FavoritesPage = () => {
11 | const [favorites, setFavorites] = useState([]);
12 | const [loading, setLoading] = useState(false);
13 | const [error, setError] = useState(false);
14 | const { user, token } = useSelector((state) => state.user);
15 | const dispatch = useDispatch();
16 |
17 | const fetchFavorites = async () => {
18 | setLoading(true);
19 | setError(false);
20 | await client
21 | .get("/users/favorites", {
22 | headers: {
23 | Authorization: `Bearer ${token}`,
24 | },
25 | })
26 | .then((res) => {
27 | setLoading(false);
28 | setFavorites(res.data);
29 | })
30 | .catch(() => {
31 | setLoading(false);
32 | setError(true);
33 | });
34 | };
35 |
36 | useEffect(() => {
37 | token && fetchFavorites();
38 | }, []);
39 |
40 | const onPlay = (song) => {
41 | const index = favorites?.findIndex((s) => s._id == song._id);
42 |
43 | dispatch(setTrackList({ list: favorites, index }));
44 | dispatch(playTrack(song));
45 | };
46 |
47 | if (!user) {
48 | return (
49 |
50 |
51 |
52 | Please login to see your favorites
53 |
54 |
55 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | return (
65 |
71 |
72 |
76 | Favorites
77 |
78 |
79 | Your favorite songs
80 |
81 |
82 |
83 | {loading && favorites.length < 1 && (
84 |
85 |
86 |
87 | )}
88 | {error && (
89 |
90 | Sorry, an error occured
91 |
92 | )}
93 |
94 | {favorites?.map((song) => (
95 |
96 | ))}
97 |
98 | {!loading && !error && favorites.length < 1 && (
99 | {"You haven't liked any songs yet..."}
100 | )}
101 |
102 | );
103 | };
104 |
105 | export default FavoritesPage;
106 |
--------------------------------------------------------------------------------
/server/controllers/songController.js:
--------------------------------------------------------------------------------
1 | import Song from "../models/Song.js";
2 | import User from "../models/User.js";
3 |
4 | //@desc Get all the songs
5 | //@route GET /api/songs
6 | //@access public
7 | const getSongs = async (req, res) => {
8 | const songs = await Song.find({});
9 |
10 | if (!songs) {
11 | return res.status(400).json({ message: "An error occured!" });
12 | }
13 | const shuffledSongs = songs.sort(() => (Math.random() > 0.5 ? 1 : -1));
14 |
15 | res.status(200).json(shuffledSongs);
16 | };
17 |
18 | //@desc Get the top songs
19 | //@route GET /api/songs/top
20 | //@access public
21 | const getTopSongs = async (req, res) => {
22 | try {
23 | const results = await Song.aggregate([
24 | {
25 | $project: {
26 | title: 1,
27 | duration: 1,
28 | coverImage: 1,
29 | artistes: 1,
30 | songUrl: 1,
31 | artistIds: 1,
32 | type: 1,
33 | likes: {
34 | $size: {
35 | $objectToArray: "$likes",
36 | },
37 | },
38 | },
39 | },
40 | { $sort: { likes: -1 } },
41 | { $limit: 8 },
42 | ]);
43 | res.status(200).json(results);
44 | } catch (error) {
45 | res.status(400).json({ message: error.message });
46 | }
47 | };
48 |
49 | //@desc Get the new releases
50 | //@route GET /api/songs/releases
51 | //@access public
52 | const getNewReleases = async (req, res) => {
53 | const songs = await Song.find({});
54 |
55 | const result = songs.slice(-11, -1);
56 | const shuffledSongs = result.sort(() => (Math.random() > 0.5 ? 1 : -1));
57 |
58 | res.status(200).json(shuffledSongs);
59 | };
60 |
61 | //@desc Get random songs
62 | //@route GET /api/songs/random
63 | //@access public
64 | const getRandom = async (req, res) => {
65 | const songs = await Song.find({});
66 |
67 | const shuffledSongs = songs.sort(() => (Math.random() > 0.5 ? 1 : -1));
68 | const result = shuffledSongs.slice(-11, -1);
69 |
70 | res.status(200).json(result);
71 | };
72 |
73 | //@desc Get the popular songs around you
74 | //@route GET /api/songs/popular
75 | //@access public
76 | const getAroundYou = async (req, res) => {
77 | const songs = await Song.find({});
78 |
79 | const result = songs.slice(0, 11);
80 | const shuffledSongs = result.sort(() => (Math.random() > 0.5 ? 1 : -1));
81 |
82 | res.status(200).json(shuffledSongs);
83 | };
84 |
85 | //@desc Like or unlike a song
86 | //@route PATCH /api/songs/like/:id
87 | //@access private
88 | const likeSong = async (req, res) => {
89 | try {
90 | const { id } = req.params;
91 | const userId = req.user.id;
92 | const song = await Song.findById(id);
93 | const user = await User.findById(userId);
94 |
95 | if (!user) {
96 | return res.json(404).json({ message: "User not found!" });
97 | }
98 | if (!song) {
99 | return res.json(404).json({ message: "Song not found!" });
100 | }
101 |
102 | const isLiked = song.likes.get(userId);
103 |
104 | if (isLiked) {
105 | song.likes.delete(userId);
106 | user.favorites = user.favorites.filter((songId) => songId !== id);
107 | } else {
108 | song.likes.set(userId, true);
109 | user.favorites.push(id);
110 | }
111 |
112 | const savedSong = await song.save();
113 | const savedUser = await user.save();
114 |
115 | if (!savedSong || !savedUser) {
116 | return res.status(400).json({ message: "An error occured" });
117 | }
118 |
119 | const returnUser = {
120 | id: savedUser.id,
121 | username: savedUser.username,
122 | favorites: savedUser.favorites,
123 | playlists: savedUser.playlists,
124 | };
125 |
126 | res.status(200).json(returnUser);
127 | } catch (error) {
128 | return res.status(409).json({ message: error.message });
129 | }
130 | };
131 |
132 | export {
133 | getSongs,
134 | getTopSongs,
135 | getNewReleases,
136 | getRandom,
137 | getAroundYou,
138 | likeSong,
139 | };
140 |
--------------------------------------------------------------------------------
/client/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/controllers/playlistController.js:
--------------------------------------------------------------------------------
1 | import Playlist from "../models/Playlist.js";
2 | import Song from "../models/Song.js";
3 | import User from "../models/User.js";
4 |
5 | //@desc Get all the playlists
6 | //@route GET /api/playlists
7 | //@access public
8 | const getPlaylists = async (req, res) => {
9 | const playlists = await Playlist.find({});
10 |
11 | if (!playlists) {
12 | return res.status(400).json({ message: "An error occured" });
13 | }
14 |
15 | res.status(200).json(playlists);
16 | };
17 |
18 | //@desc Create a playlist
19 | //@route POST /api/playlists/create
20 | //@access private
21 | const createPlaylist = async (req, res) => {
22 | const { id, username } = req.user;
23 | const { title, description, isPrivate, songIds } = req.body;
24 | const user = await User.findById(id);
25 |
26 | if (!title || !songIds) {
27 | return res.status(400).json({ message: "All fields are required!" });
28 | }
29 |
30 | if (!user) {
31 | return res.status(404).json({ message: "User not found!" });
32 | }
33 |
34 | await Promise.all(
35 | songIds.map(async (id) => {
36 | const songExists = await Song.findById(id);
37 | if (!songExists) {
38 | return res.status(404).json({ message: "Song not found" });
39 | }
40 | })
41 | );
42 |
43 | const newPlaylist = await Playlist.create({
44 | title,
45 | description,
46 | userId: id,
47 | userName: username,
48 | songs: songIds,
49 | isPrivate,
50 | });
51 |
52 | if (!newPlaylist) {
53 | return res.status(400).json({ message: "An error occured!" });
54 | }
55 |
56 | user.playlists.push(newPlaylist.id);
57 | await user.save();
58 |
59 | res.status(201).json(newPlaylist);
60 | };
61 |
62 | //@desc Get a playlists' details
63 | //@route GET /api/playlists/:id
64 | //@access public
65 | const getPlaylist = async (req, res) => {
66 | try {
67 | const { id } = req.params;
68 |
69 | const playlist = await Playlist.findById(id);
70 |
71 | if (!playlist) {
72 | return res.status(404).json({ message: "Playlist not found!" });
73 | }
74 |
75 | let songs = [];
76 |
77 | await Promise.all(
78 | playlist.songs.map(async (songId) => {
79 | const playlistSong = await Song.findById(songId);
80 | if (!playlistSong) {
81 | return res.status(404).json({ message: "Song not found" });
82 | } else {
83 | songs.push(playlistSong);
84 | }
85 | })
86 | );
87 |
88 | res.status(200).json({ ...playlist.toObject(), songs });
89 | } catch (error) {
90 | res.status(400).json({ message: error.message });
91 | }
92 | };
93 |
94 | //@desc Add or remove a song from a playlist
95 | //@route PATCH /api/playlists/:id
96 | //@access private
97 | const editPlaylist = async (req, res) => {
98 | const { id } = req.params;
99 | const userId = req.user.id;
100 | const { title, description, songIds } = req.body;
101 | const playlist = await Playlist.findById(id);
102 |
103 | if (!title || !songIds) {
104 | return res.status(400).json({ message: "All fields are required!" });
105 | }
106 |
107 | if (!playlist) {
108 | return res.status(400).json({ message: "Playlist not found!" });
109 | }
110 |
111 | if (playlist.userId !== userId) {
112 | return res
113 | .status(403)
114 | .json({ message: "You are not allowed to edit other users' playlists!" });
115 | }
116 |
117 | await Promise.all(
118 | songIds.map(async (id) => {
119 | const songExists = await Song.findById(id);
120 | if (!songExists) {
121 | return res.status(404).json({ message: "Song not found" });
122 | }
123 | })
124 | );
125 |
126 | const updatedPlaylist = await Playlist.findByIdAndUpdate(
127 | id,
128 | { title, description, songs: songIds },
129 | {
130 | new: true,
131 | }
132 | );
133 |
134 | if (!updatedPlaylist) {
135 | return res.status(400).json({ message: "Playlist not updated!" });
136 | }
137 | res.status(200).json(updatedPlaylist);
138 | };
139 |
140 | export { getPlaylists, createPlaylist, getPlaylist, editPlaylist };
141 |
--------------------------------------------------------------------------------
/client/src/components/ArtisteSong.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Flex,
5 | Hide,
6 | Image,
7 | Text,
8 | useDisclosure,
9 | useToast,
10 | } from "@chakra-ui/react";
11 | import { useRef } from "react";
12 | import { AiFillHeart, AiFillPlayCircle, AiOutlineHeart } from "react-icons/ai";
13 | import { useDispatch, useSelector } from "react-redux";
14 | import { BsSoundwave } from "react-icons/bs";
15 | import LoginModal from "./LoginModal";
16 | import { client } from "../api";
17 | import { setUser } from "../redux/slices/userSlice";
18 | import { setModalMessage } from "../redux/slices/modalSlice";
19 |
20 | const ArtisteSong = ({ song, handlePlay }) => {
21 | const dispatch = useDispatch();
22 | const { currentTrack, isPlaying } = useSelector((state) => state.player);
23 | const { user, token } = useSelector((state) => state.user);
24 | const { isOpen, onOpen, onClose } = useDisclosure();
25 | const modalRef = useRef();
26 | const toast = useToast();
27 |
28 | const isCurrentTrack = currentTrack?._id === song?._id;
29 |
30 | const playSong = () => {
31 | handlePlay(song);
32 | };
33 |
34 | const likeSong = async () => {
35 | await client
36 | .patch(`/songs/like/${song?._id}`, null, {
37 | headers: {
38 | Authorization: `Bearer ${token}`,
39 | },
40 | })
41 | .then((res) => {
42 | dispatch(setUser(res.data));
43 | toast({
44 | description: "Your favorites have been updated",
45 | status: "success",
46 | });
47 | })
48 | .catch(() => {
49 | toast({
50 | description: "An error occured",
51 | status: "error",
52 | });
53 | });
54 | };
55 |
56 | const handleLike = () => {
57 | if (!token) {
58 | dispatch(
59 | setModalMessage("Please login to save songs to your favorites.")
60 | );
61 | onOpen();
62 | } else {
63 | likeSong();
64 | }
65 | };
66 |
67 | return (
68 | <>
69 |
70 |
78 |
79 |
87 |
88 |
89 | {song?.title}
90 | {isCurrentTrack && (
91 |
92 |
93 |
98 | Playing
99 |
100 |
101 | )}
102 |
103 |
104 | {song?.artistes.join(", ")}
105 |
106 |
107 |
108 |
109 | {isCurrentTrack && isPlaying ? null : (
110 |
121 | )}
122 |
123 |
124 | {song?.duration?.split(".")?.join(":")}
125 |
126 |
127 |
139 |
140 |
141 | >
142 | );
143 | };
144 |
145 | export default ArtisteSong;
146 |
--------------------------------------------------------------------------------
/client/src/pages/ArtistePage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import {
4 | Box,
5 | Button,
6 | Divider,
7 | Flex,
8 | Heading,
9 | Image,
10 | Text,
11 | } from "@chakra-ui/react";
12 | import { client } from "../api";
13 | import ArtisteSong from "../components/ArtisteSong";
14 | import { BsFillPlayFill } from "react-icons/bs";
15 | import { MdErrorOutline } from "react-icons/md";
16 | import { useDispatch } from "react-redux";
17 | import { playTrack, setTrackList } from "../redux/slices/playerSlice";
18 | import LoadingSkeleton from "../components/LoadingSkeleton";
19 |
20 | const ArtistePage = () => {
21 | const { id } = useParams();
22 | const dispatch = useDispatch();
23 |
24 | const [artiste, setArtiste] = useState([]);
25 | const [loading, setLoading] = useState(true);
26 | const [error, setError] = useState(false);
27 |
28 | const fetchArtiste = async () => {
29 | setLoading(true);
30 | setError(false);
31 | await client
32 | .get(`/artistes/${id}`)
33 | .then((res) => {
34 | setArtiste(res.data);
35 | setLoading(false);
36 | })
37 | .catch(() => {
38 | setError(true);
39 | setLoading(false);
40 | });
41 | };
42 |
43 | useEffect(() => {
44 | fetchArtiste();
45 | }, []);
46 |
47 | const handlePlay = () => {
48 | dispatch(setTrackList({ list: artiste?.songs }));
49 | dispatch(playTrack(artiste?.songs[0]));
50 | };
51 |
52 | const onSongPlay = (song) => {
53 | const index = artiste?.songs.findIndex((s) => s._id == song._id);
54 |
55 | dispatch(setTrackList({ list: artiste?.songs, index }));
56 | dispatch(playTrack(song));
57 | };
58 |
59 | if (loading) {
60 | return ;
61 | }
62 |
63 | if (error) {
64 | return (
65 |
66 |
67 |
68 |
69 | An error occured
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | return (
77 |
78 |
79 |
85 |
86 |
94 |
95 |
96 |
102 | {artiste?.name}
103 |
104 |
108 | {artiste?.bio}
109 |
110 |
111 |
112 |
113 |
114 |
118 | Songs
119 |
120 | }>
132 | Play All
133 |
134 |
135 |
136 |
137 |
138 | {artiste?.songs?.map((song) => (
139 |
144 | ))}
145 |
146 |
147 |
148 |
149 | );
150 | };
151 |
152 | export default ArtistePage;
153 |
--------------------------------------------------------------------------------
/client/src/components/SongCard.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Flex,
5 | Heading,
6 | Image,
7 | Text,
8 | useToast,
9 | } from "@chakra-ui/react";
10 | import { useDispatch, useSelector } from "react-redux";
11 | import { motion } from "framer-motion";
12 | import { fadeInUp } from "../theme/motionVariants";
13 | import {
14 | setCurrentTrack,
15 | setPlaying,
16 | setTrackList,
17 | } from "../redux/slices/playerSlice";
18 | import {
19 | AiFillHeart,
20 | AiFillPauseCircle,
21 | AiFillPlayCircle,
22 | AiOutlineHeart,
23 | } from "react-icons/ai";
24 | import { Link } from "react-router-dom";
25 | import { client } from "../api";
26 | import { setUser } from "../redux/slices/userSlice";
27 |
28 | const SongCard = ({ song }) => {
29 | const dispatch = useDispatch();
30 | const { currentTrack, isPlaying } = useSelector((state) => state.player);
31 | const { user, token } = useSelector((state) => state.user);
32 |
33 | const toast = useToast();
34 |
35 | const playSong = () => {
36 | dispatch(setCurrentTrack(song));
37 | dispatch(setTrackList({ list: [song] }));
38 | dispatch(setPlaying(true));
39 | };
40 |
41 | const handleLike = async () => {
42 | await client
43 | .patch(`/songs/like/${song?._id}`, null, {
44 | headers: {
45 | Authorization: `Bearer ${token}`,
46 | },
47 | })
48 | .then((res) => {
49 | dispatch(setUser(res.data));
50 | toast({
51 | description: "Your favorites have been updated",
52 | status: "success",
53 | });
54 | })
55 | .catch(() => {
56 | toast({
57 | description: "An error occured",
58 | status: "error",
59 | });
60 | });
61 | };
62 |
63 | const isCurrentTrack = currentTrack?._id === song?._id;
64 | const isFavorite = user?.favorites.includes(song._id);
65 |
66 | return (
67 |
78 |
85 |
93 |
106 |
120 |
121 |
122 |
123 |
124 |
129 | {song?.title}
130 |
131 |
132 |
136 | {" "}
137 | {song?.artistes.join(", ")}{" "}
138 |
139 |
140 |
141 | {user && (
142 |
154 | )}
155 |
156 |
157 | );
158 | };
159 |
160 | export default SongCard;
161 |
--------------------------------------------------------------------------------
/client/src/pages/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Flex,
5 | FormControl,
6 | FormLabel,
7 | Heading,
8 | Input,
9 | InputGroup,
10 | InputRightElement,
11 | Spinner,
12 | Text,
13 | } from "@chakra-ui/react";
14 | import { useState } from "react";
15 | import { AiOutlineEye, AiOutlineEyeInvisible } from "react-icons/ai";
16 | import { MdError } from "react-icons/md";
17 | import { useDispatch } from "react-redux";
18 | import { Link } from "react-router-dom";
19 | import { client } from "../api";
20 | import { loginUser } from "../redux/slices/userSlice";
21 | import { resetPlayer } from "../redux/slices/playerSlice";
22 |
23 | const LoginPage = () => {
24 | const [error, setError] = useState(null);
25 | const [loading, setLoading] = useState(false);
26 | const [username, setUsername] = useState("");
27 | const [password, setPassword] = useState("");
28 | const [showPassword, setShowPassword] = useState(false);
29 | const dispatch = useDispatch();
30 |
31 | const validateFields = () => {
32 | if (username == "" || password == "") {
33 | setError("All fields are required!");
34 | return false;
35 | } else {
36 | setError(null);
37 | return true;
38 | }
39 | };
40 |
41 | const handleLogin = async () => {
42 | if (validateFields()) {
43 | setLoading(true);
44 | await client
45 | .post("/users/login", {
46 | username,
47 | password,
48 | })
49 | .then((res) => {
50 | dispatch(resetPlayer());
51 | dispatch(loginUser(res.data));
52 | setLoading(false);
53 | })
54 | .catch((err) => {
55 | setError(err?.response?.data?.message);
56 | setLoading(false);
57 | });
58 | }
59 | };
60 |
61 | return (
62 |
63 |
67 |
68 |
69 | Login
70 |
71 | To continue enjoying BeatBox
72 |
73 |
74 |
75 |
76 | Username
77 |
78 | setUsername(e.target.value)}
88 | />
89 |
90 |
91 |
92 | Password
93 |
94 |
95 | setPassword(e.target.value)}
103 | />
104 |
105 |
113 |
114 |
115 |
116 | {error && (
117 |
118 |
119 |
120 | {error}
121 |
122 |
123 | )}
124 |
125 |
133 |
134 | OR
135 |
136 |
137 |
138 | Continue without logging in
139 |
140 |
141 |
142 |
143 | {"Don't have an account yet?"}{" "}
144 |
145 | {" "}
146 |
147 | Register
148 |
149 |
150 |
151 |
152 |
153 |
154 | );
155 | };
156 |
157 | export default LoginPage;
158 |
--------------------------------------------------------------------------------
/client/src/pages/RegisterPage.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Flex,
5 | FormControl,
6 | FormLabel,
7 | Heading,
8 | Input,
9 | InputGroup,
10 | InputRightElement,
11 | Spinner,
12 | Text,
13 | } from "@chakra-ui/react";
14 | import { useState } from "react";
15 | import { AiOutlineEye, AiOutlineEyeInvisible } from "react-icons/ai";
16 | import { MdError } from "react-icons/md";
17 | import { Link } from "react-router-dom";
18 | import { client } from "../api";
19 | import { useDispatch } from "react-redux";
20 | import { loginUser } from "../redux/slices/userSlice";
21 | import { resetPlayer } from "../redux/slices/playerSlice";
22 |
23 | const RegisterPage = () => {
24 | const [error, setError] = useState(null);
25 | const [loading, setLoading] = useState(false);
26 | const [username, setUsername] = useState("");
27 | const [password, setPassword] = useState("");
28 | const [showPassword, setShowPassword] = useState(false);
29 | const dispatch = useDispatch();
30 |
31 | const validateFields = () => {
32 | if (username == "" || password == "") {
33 | setError("All fields are required!");
34 | return false;
35 | } else {
36 | setError(null);
37 | return true;
38 | }
39 | };
40 |
41 | const handleRegister = async () => {
42 | if (validateFields()) {
43 | setLoading(true);
44 | await client
45 | .post("/users/register", {
46 | username,
47 | password,
48 | })
49 | .then((res) => {
50 | dispatch(resetPlayer());
51 | dispatch(loginUser(res.data));
52 | setLoading(false);
53 | })
54 | .catch((err) => {
55 | setError(err?.response?.data?.message);
56 | setLoading(false);
57 | });
58 | }
59 | };
60 |
61 | return (
62 |
63 |
67 |
68 |
69 | Register
70 |
71 | To continue enjoying BeatBox
72 |
73 |
74 |
75 |
76 | Username
77 |
78 | setUsername(e.target.value)}
88 | />
89 |
90 |
91 |
92 | Password
93 |
94 |
95 | setPassword(e.target.value)}
103 | />
104 |
105 |
113 |
114 |
115 |
116 | {error && (
117 |
118 |
119 |
120 | {error}
121 |
122 |
123 | )}
124 |
125 |
133 |
134 | OR
135 |
136 |
137 |
138 | Continue without logging in
139 |
140 |
141 |
142 |
143 | Already have an account ?{" "}
144 |
145 | {" "}
146 |
147 | Login
148 |
149 |
150 |
151 |
152 |
153 |
154 | );
155 | };
156 |
157 | export default RegisterPage;
158 |
--------------------------------------------------------------------------------
/client/src/pages/PlaylistPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { client } from "../api";
3 | import { useParams, Link } from "react-router-dom";
4 | import LoadingSkeleton from "../components/LoadingSkeleton";
5 | import { MdErrorOutline } from "react-icons/md";
6 | import {
7 | Box,
8 | Button,
9 | Divider,
10 | Flex,
11 | Heading,
12 | Image,
13 | Text,
14 | } from "@chakra-ui/react";
15 | import ArtisteSong from "../components/ArtisteSong";
16 | import { useDispatch, useSelector } from "react-redux";
17 | import { playTrack, setTrackList } from "../redux/slices/playerSlice";
18 | import { BsFillPlayFill } from "react-icons/bs";
19 | import { AiFillEdit } from "react-icons/ai";
20 |
21 | const PlaylistPage = () => {
22 | const [data, setData] = useState(null);
23 | const [loading, setLoading] = useState(false);
24 | const [error, setError] = useState(false);
25 |
26 | const { id } = useParams();
27 |
28 | const dispatch = useDispatch();
29 | const { user } = useSelector((state) => state.user);
30 | const isUserPlaylist = user?.id === data?.userId;
31 |
32 | const fetchPlaylist = async () => {
33 | setLoading(true);
34 | setError(false);
35 | await client
36 | .get(`/playlists/${id}`)
37 | .then((res) => {
38 | setData(res.data);
39 | setLoading(false);
40 | })
41 | .catch(() => {
42 | setError(true);
43 | setLoading(false);
44 | });
45 | };
46 |
47 | useEffect(() => {
48 | fetchPlaylist();
49 | }, []);
50 |
51 | const handlePlay = () => {
52 | dispatch(setTrackList({ list: data?.songs }));
53 | dispatch(playTrack(data?.songs[0]));
54 | };
55 |
56 | const onSongPlay = (song) => {
57 | const index = data?.songs.findIndex((s) => s._id == song._id);
58 |
59 | dispatch(setTrackList({ list: data?.songs, index }));
60 | dispatch(playTrack(song));
61 | };
62 |
63 | if (loading) {
64 | return ;
65 | }
66 |
67 | if (error) {
68 | return (
69 |
70 |
71 |
72 |
73 | An error occured
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | return (
81 |
87 |
88 |
93 |
94 |
102 |
103 |
104 |
110 | Playlist
111 |
112 |
118 | {data?.title}
119 |
120 |
121 | {data?.description}
122 |
123 | {isUserPlaylist && (
124 |
125 | }
128 | size="sm"
129 | mt={2}
130 | _hover={{}}>
131 | Edit
132 |
133 |
134 | )}
135 |
136 |
137 |
138 |
139 |
143 | {data?.songs?.length} Songs
144 |
145 | }>
157 | Play All
158 |
159 |
160 |
161 |
162 |
163 | {data?.songs?.map((song) => (
164 |
169 | ))}
170 |
171 |
172 |
173 |
174 | );
175 | };
176 |
177 | export default PlaylistPage;
178 |
--------------------------------------------------------------------------------
/client/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { BiMenuAltRight, BiMusic } from "react-icons/bi";
2 | import { AiFillHeart, AiFillHome, AiOutlineLogout } from "react-icons/ai";
3 | import { BsHeadphones } from "react-icons/bs";
4 | import { TiTimes } from "react-icons/ti";
5 | import { HiOutlineUserCircle, HiViewGrid } from "react-icons/hi";
6 | import { Link, NavLink, useLocation, useNavigate } from "react-router-dom";
7 | import {
8 | Box,
9 | Button,
10 | Divider,
11 | Flex,
12 | Heading,
13 | Hide,
14 | Show,
15 | Text,
16 | } from "@chakra-ui/react";
17 | import { useDispatch, useSelector } from "react-redux";
18 | import { logoutUser } from "../redux/slices/userSlice";
19 | import { resetPlayer } from "../redux/slices/playerSlice";
20 | import { useEffect, useState } from "react";
21 |
22 | const MobileNav = () => {
23 | const [navIsOpen, setNavIsOpen] = useState(false);
24 | const { pathname } = useLocation();
25 |
26 | useEffect(() => {
27 | setNavIsOpen(false);
28 | }, [pathname]);
29 |
30 | const toggleNav = () => {
31 | setNavIsOpen(!navIsOpen);
32 | };
33 |
34 | return (
35 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | BeatBox
50 |
51 |
52 |
53 |
56 |
57 | {navIsOpen && (
58 |
59 |
60 |
61 | )}
62 |
63 | );
64 | };
65 |
66 | const DesktopNav = () => {
67 | return (
68 |
78 |
79 |
80 |
81 |
82 | BeatBox
83 |
84 |
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | const NavContent = () => {
92 | const { user } = useSelector((state) => state.user);
93 | const dispatch = useDispatch();
94 | const navigate = useNavigate();
95 |
96 | const handleLogout = () => {
97 | dispatch(resetPlayer());
98 | dispatch(logoutUser());
99 | navigate("/auth/login");
100 | };
101 |
102 | const gotoLogin = () => {
103 | dispatch(resetPlayer());
104 | navigate("/auth/login");
105 | };
106 | return (
107 |
108 |
109 |
110 | {({ isActive }) => (
111 |
127 | )}
128 |
129 |
130 | {({ isActive }) => (
131 |
147 | )}
148 |
149 |
150 | {({ isActive }) => (
151 |
167 | )}
168 |
169 |
170 | {({ isActive }) => (
171 |
187 | )}
188 |
189 |
190 |
197 |
198 | {user ? (
199 |
200 |
201 |
202 |
203 | {user?.username}
204 |
205 |
206 |
217 |
218 | ) : (
219 |
231 | )}
232 |
233 |
234 | );
235 | };
236 |
237 | const Navbar = () => {
238 | return (
239 | <>
240 |
241 |
242 |
243 |
244 |
245 |
246 | >
247 | );
248 | };
249 |
250 | export default Navbar;
251 |
--------------------------------------------------------------------------------
/client/src/components/MusicPlayer/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import {
3 | Button,
4 | Flex,
5 | Hide,
6 | SimpleGrid,
7 | useDisclosure,
8 | useToast,
9 | } from "@chakra-ui/react";
10 | import { useDispatch, useSelector } from "react-redux";
11 | import { AiFillHeart, AiOutlineHeart } from "react-icons/ai";
12 | import {
13 | nextTrack,
14 | prevTrack,
15 | setPlaying,
16 | } from "../../redux/slices/playerSlice";
17 | import { client } from "../../api";
18 | import { setUser } from "../../redux/slices/userSlice";
19 | import VolumeControl from "./VolumeControl";
20 | import TrackDetails from "./TrackDetails";
21 | import PlayControls from "./PlayControls";
22 | import LoginModal from "../LoginModal";
23 | import PlayingBar from "./PlayingBar";
24 | import { setModalMessage } from "../../redux/slices/modalSlice";
25 |
26 | const MusicPlayer = () => {
27 | const { isOpen, onOpen, onClose } = useDisclosure();
28 | const modalRef = useRef();
29 | const toast = useToast();
30 | const dispatch = useDispatch();
31 | const { currentTrack, repeatStatus, currentIndex, trackList, isPlaying } =
32 | useSelector((state) => state.player);
33 | const { user, token } = useSelector((state) => state.user);
34 | const audioRef = useRef();
35 |
36 | const isEndOfTracklist = currentIndex === trackList.length - 1;
37 |
38 | const [songDetails, setSongDetails] = useState(null);
39 | const [audioPlaying, setAudioPlaying] = useState(
40 | audioRef.current && audioRef.current.playing
41 | );
42 |
43 | useEffect(() => {
44 | if (audioPlaying) {
45 | dispatch(setPlaying(true));
46 | } else {
47 | dispatch(setPlaying(false));
48 | }
49 | }, [audioPlaying]);
50 |
51 | useEffect(() => {
52 | if (isPlaying) {
53 | audioRef.current.play();
54 | }
55 | }, [isPlaying]);
56 |
57 | useEffect(() => {
58 | setSongDetails((prev) => {
59 | return { ...prev, time: 0 };
60 | });
61 | audioRef.current.currentTime = 0;
62 | audioRef.current.play();
63 | }, [currentTrack?._id]);
64 |
65 | useEffect(() => {
66 | setSongDetails({
67 | volume: 1,
68 | time: audioRef?.current
69 | ? Math.round(
70 | (audioRef?.current.currentTime / audioRef.current.duration) * 100
71 | ) // eslint-disable-line no-mixed-spaces-and-tabs
72 | : 0,
73 | shuffle: false,
74 | repeat: false,
75 | });
76 | }, [audioRef.current]);
77 |
78 | const seekPoint = (e) => {
79 | audioRef.current.currentTime = (e / 100) * audioRef.current.duration;
80 |
81 | setSongDetails((prev) => ({
82 | ...prev,
83 | time: Math.round(
84 | (audioRef.current.currentTime / audioRef.current.duration) * 100
85 | ),
86 | }));
87 | };
88 |
89 | const changeVolume = (e) => {
90 | setSongDetails((prevValues) => {
91 | return { ...prevValues, volume: e / 100 };
92 | });
93 | audioRef.current.volume = e / 100;
94 | };
95 |
96 | const handlePlayPause = () => {
97 | if (isPlaying) {
98 | audioRef?.current.pause();
99 | dispatch(setPlaying(false));
100 | } else {
101 | audioRef?.current.play();
102 | dispatch(setPlaying(true));
103 | }
104 | };
105 |
106 | const volumeToggle = () => {
107 | if (songDetails?.volume > 0) {
108 | setSongDetails((prev) => {
109 | return { ...prev, volume: 0 };
110 | });
111 | audioRef.current.volume = 0;
112 | } else {
113 | setSongDetails((prev) => {
114 | return { ...prev, volume: 1 };
115 | });
116 | audioRef.current.volume = 1;
117 | }
118 | };
119 |
120 | useEffect(() => {
121 | audioRef.current.currentTime = 0;
122 | audioRef?.current.play();
123 | dispatch(setPlaying(true));
124 | }, [currentTrack.src]);
125 |
126 | const handleNextSong = () => {
127 | if (trackList.length == 1) {
128 | restartSong();
129 | } else {
130 | dispatch(nextTrack());
131 | }
132 | };
133 |
134 | const handlePreviousSong = () => {
135 | if (trackList.length == 1) {
136 | restartSong();
137 | } else {
138 | dispatch(prevTrack());
139 | }
140 | };
141 |
142 | const restartSong = () => {
143 | setSongDetails((prev) => {
144 | return { ...prev, time: 0 };
145 | });
146 | audioRef.current.currentTime = 0;
147 | audioRef.current.play();
148 | };
149 |
150 | const handleEnded = () => {
151 | switch (repeatStatus) {
152 | case "OFF":
153 | if (!isEndOfTracklist) {
154 | handleNextSong();
155 | }
156 | break;
157 | case "TRACKLIST":
158 | handleNextSong();
159 | break;
160 | case "SINGLE":
161 | audioRef.current.play();
162 | break;
163 |
164 | default:
165 | break;
166 | }
167 | };
168 |
169 | const likeSong = async () => {
170 | await client
171 | .patch(`/songs/like/${currentTrack?._id}`, null, {
172 | headers: {
173 | Authorization: `Bearer ${token}`,
174 | },
175 | })
176 | .then((res) => {
177 | dispatch(setUser(res.data));
178 | toast({
179 | description: "Your favorites have been updated",
180 | status: "success",
181 | });
182 | })
183 | .catch(() => {
184 | toast({
185 | description: "An error occured",
186 | status: "error",
187 | });
188 | });
189 | };
190 |
191 | const handleLike = () => {
192 | if (!token) {
193 | dispatch(
194 | setModalMessage("Please login to save songs to your favorites.")
195 | );
196 | onOpen();
197 | } else {
198 | likeSong();
199 | }
200 | };
201 |
202 | return (
203 | <>
204 |
205 |
220 |
221 |
222 |
229 |
230 |
236 |
237 |
238 |
239 |
256 |
257 |
258 |
263 |
264 |
281 |
282 |
283 | >
284 | );
285 | };
286 |
287 | export { MusicPlayer };
288 |
--------------------------------------------------------------------------------
/client/src/pages/EditPlaylistPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useNavigate, useParams } from "react-router-dom";
3 | import {
4 | Box,
5 | Button,
6 | Divider,
7 | Flex,
8 | FormControl,
9 | FormLabel,
10 | Heading,
11 | Input,
12 | SimpleGrid,
13 | Spinner,
14 | Text,
15 | useToast,
16 | } from "@chakra-ui/react";
17 | import { client } from "../api";
18 | import PlaylistSong from "../components/PlaylistSong";
19 | import { useSelector } from "react-redux";
20 | import { AiOutlineLoading } from "react-icons/ai";
21 |
22 | const EditPlaylistPage = () => {
23 | const [fetchPlaylistStatus, setFetchPlaylistStatus] = useState({
24 | loading: false,
25 | error: false,
26 | });
27 | const [otherSongs, setOtherSongs] = useState({
28 | loading: false,
29 | error: false,
30 | data: [],
31 | });
32 | const [editLoading, setEditLoading] = useState(false);
33 | const [playlistSongs, setPlaylistSongs] = useState([]);
34 | const [inputs, setInputs] = useState({
35 | playlistName: "",
36 | playlistDesc: "",
37 | });
38 |
39 | const { token } = useSelector((state) => state.user);
40 |
41 | const { id } = useParams();
42 | const navigate = useNavigate();
43 | const toast = useToast();
44 |
45 | const fetchPlaylist = async () => {
46 | setFetchPlaylistStatus({ loading: true, error: false });
47 | await client
48 | .get(`/playlists/${id}`)
49 | .then((res) => {
50 | setInputs({
51 | playlistName: res.data.title,
52 | playlistDesc: res.data.description,
53 | });
54 | setPlaylistSongs(res.data.songs);
55 |
56 | setFetchPlaylistStatus({ loading: false, error: false });
57 | })
58 | .catch(() => {
59 | setFetchPlaylistStatus({ loading: false, error: true });
60 | });
61 | };
62 |
63 | const fetchOtherSongs = async () => {
64 | setOtherSongs((prev) => {
65 | return { ...prev, error: false, loading: true };
66 | });
67 | await client
68 | .get("/songs/random")
69 | .then((res) => {
70 | setOtherSongs((prev) => {
71 | return { ...prev, data: res.data, loading: false };
72 | });
73 | })
74 | .catch(() => {
75 | setOtherSongs((prev) => {
76 | return { ...prev, error: true, loading: false };
77 | });
78 | });
79 | };
80 |
81 | useEffect(() => {
82 | fetchPlaylist();
83 | fetchOtherSongs();
84 | }, []);
85 |
86 | const songIsInPlaylist = (song) => {
87 | const songExists = playlistSongs.find((s) => song._id === s._id);
88 | return songExists ? true : false;
89 | };
90 |
91 | const toggleAddSong = (song) => {
92 | if (songIsInPlaylist(song)) {
93 | setPlaylistSongs(
94 | playlistSongs.filter((currentSong) => currentSong._id !== song._id)
95 | );
96 | } else {
97 | setPlaylistSongs([...playlistSongs, song]);
98 | }
99 | };
100 |
101 | const canEditPlaylist = () => {
102 | if (inputs.playlistName == "") {
103 | return false;
104 | }
105 | if (playlistSongs.length < 1) {
106 | return false;
107 | }
108 |
109 | return true;
110 | };
111 |
112 | const handleEditPlaylist = async () => {
113 | if (!canEditPlaylist()) {
114 | toast({
115 | description:
116 | inputs.playlistName == ""
117 | ? "Give your playlist a name!"
118 | : "Add songs to your playlist!",
119 | status: "error",
120 | });
121 | } else {
122 | editPlaylist();
123 | }
124 | };
125 |
126 | const editPlaylist = async () => {
127 | setEditLoading(true);
128 | const songIds = playlistSongs.map((song) => song._id);
129 | const playlistDetails = {
130 | title: inputs.playlistName,
131 | description: inputs.playlistDesc,
132 | songIds,
133 | };
134 | await client
135 | .patch(`/playlists/${id}`, playlistDetails, {
136 | headers: {
137 | Authorization: `Bearer ${token}`,
138 | "Content-Type": "application/json",
139 | },
140 | })
141 | .then(() => {
142 | setEditLoading(false);
143 | toast({
144 | description: "Playlist updated!",
145 | status: "success",
146 | });
147 | navigate("/home");
148 | })
149 | .catch((err) => {
150 | setEditLoading(false);
151 | toast({
152 | description:
153 | err?.response.data.message || "Could not update playlist!",
154 | status: "error",
155 | });
156 | });
157 | };
158 |
159 | return (
160 |
161 |
162 |
163 | Edit Playlist
164 |
165 |
177 |
178 |
179 |
180 | {fetchPlaylistStatus.loading && (
181 |
187 |
188 |
189 | )}
190 | {!fetchPlaylistStatus.loading && !fetchPlaylistStatus.error && (
191 | <>
192 |
197 |
198 |
199 | Playlist Name
200 |
201 |
211 | setInputs({ ...inputs, playlistName: e.target.value })
212 | }
213 | />
214 |
215 |
216 |
217 | Playlist Description (optional)
218 |
219 |
229 | setInputs({ ...inputs, playlistDesc: e.target.value })
230 | }
231 | />
232 |
233 |
234 |
235 | Songs
236 |
237 |
238 |
239 |
243 | {playlistSongs?.map((song) => (
244 | toggleAddSong(song)}
248 | isAdded={songIsInPlaylist(song)}
249 | />
250 | ))}
251 |
252 | >
253 | )}
254 | {!fetchPlaylistStatus.loading && fetchPlaylistStatus.error && (
255 |
256 | Sorry, an error occured
257 |
258 | )}
259 |
260 |
261 | Add Other Songs
262 |
263 |
279 |
280 |
281 | {otherSongs.loading && (
282 |
288 |
289 |
290 | )}
291 | {!otherSongs.loading && otherSongs.error && (
292 |
293 | Sorry, an error occured
294 |
295 | )}
296 | {!otherSongs.loading && !otherSongs.error && (
297 |
301 | {otherSongs?.data?.map((song) => (
302 | toggleAddSong(song)}
307 | />
308 | ))}
309 |
310 | )}
311 |
312 |
313 | );
314 | };
315 |
316 | export default EditPlaylistPage;
317 |
--------------------------------------------------------------------------------
/client/src/pages/CreatePlaylistPage.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Divider,
5 | Flex,
6 | FormControl,
7 | FormLabel,
8 | Heading,
9 | Input,
10 | SimpleGrid,
11 | Spinner,
12 | Switch,
13 | Text,
14 | useToast,
15 | } from "@chakra-ui/react";
16 | import { useEffect, useState } from "react";
17 | import { useNavigate } from "react-router-dom";
18 | import { useDispatch, useSelector } from "react-redux";
19 | import PlaylistSong from "../components/PlaylistSong";
20 | import { setUser } from "../redux/slices/userSlice";
21 | import { AiOutlineLoading } from "react-icons/ai";
22 | import { client } from "../api";
23 |
24 | const CreatePlaylistPage = () => {
25 | const [favorites, setFavorites] = useState([]);
26 | const [favoritesLoading, setFavoritesLoading] = useState(false);
27 | const [otherSongs, setOtherSongs] = useState([]);
28 | const [otherSongsLoading, setOtherSongsLoading] = useState(false);
29 | const [createPlLoading, setCreatePlLoading] = useState(false);
30 | const [inputs, setInputs] = useState({
31 | playlistName: "",
32 | playlistDesc: "",
33 | isPrivate: false,
34 | });
35 | const [error, setError] = useState({ favorites: false, otherSongs: false });
36 | const [addedSongs, setAddedSongs] = useState([]);
37 | const toast = useToast();
38 | const navigate = useNavigate();
39 |
40 | const { user, token } = useSelector((state) => state.user);
41 | const dispatch = useDispatch();
42 |
43 | const fetchFavorites = async () => {
44 | setFavoritesLoading(true);
45 | setError({ ...error, favorites: false });
46 | await client
47 | .get("/users/favorites", {
48 | headers: {
49 | Authorization: `Bearer ${token}`,
50 | },
51 | })
52 | .then((res) => {
53 | setFavoritesLoading(false);
54 | setFavorites(res.data);
55 | })
56 | .catch(() => {
57 | setFavoritesLoading(false);
58 | setError({ ...error, favorites: true });
59 | });
60 | };
61 |
62 | const fetchOtherSongs = async () => {
63 | setError({ ...error, otherSongs: false });
64 | setOtherSongsLoading(true);
65 | await client
66 | .get("/songs/random")
67 | .then((res) => {
68 | setOtherSongs(res.data);
69 | setOtherSongsLoading(false);
70 | })
71 | .catch(() => {
72 | setError({ ...error, otherSongs: true });
73 | setOtherSongsLoading(false);
74 | });
75 | };
76 |
77 | useEffect(() => {
78 | fetchFavorites();
79 | fetchOtherSongs();
80 | }, []);
81 |
82 | const createPlaylist = async () => {
83 | const playlistDetails = {
84 | title: inputs.playlistName,
85 | description: inputs.playlistDesc,
86 | isPrivate: inputs.isPrivate,
87 | songIds: addedSongs,
88 | };
89 | setCreatePlLoading(true);
90 | await client
91 | .post("/playlists/create", playlistDetails, {
92 | headers: {
93 | Authorization: `Bearer ${token}`,
94 | "Content-Type": "application/json",
95 | },
96 | })
97 | .then((res) => {
98 | setCreatePlLoading(false);
99 | toast({
100 | description: "Playlist created!",
101 | status: "success",
102 | });
103 | navigate("/home");
104 | dispatch(
105 | setUser({ ...user, playlists: [...user?.playlists, res.data?._id] })
106 | );
107 | })
108 | .catch(() => {
109 | setCreatePlLoading(false);
110 | toast({
111 | description: "An error occured!",
112 | status: "error",
113 | });
114 | });
115 | };
116 |
117 | const songIsInPlaylist = (id) => {
118 | return addedSongs.includes(id);
119 | };
120 |
121 | const toggleAddSong = (id) => {
122 | if (songIsInPlaylist(id)) {
123 | setAddedSongs(addedSongs.filter((songId) => songId !== id));
124 | } else {
125 | setAddedSongs([...addedSongs, id]);
126 | }
127 | };
128 |
129 | const canCreatePlaylist = () => {
130 | if (inputs.playlistName == "") {
131 | return false;
132 | }
133 | if (addedSongs.length < 1) {
134 | return false;
135 | }
136 |
137 | return true;
138 | };
139 |
140 | const handleCreatePlaylist = async () => {
141 | if (!canCreatePlaylist()) {
142 | toast({
143 | description:
144 | inputs.playlistName == ""
145 | ? "Give your playlist a name!"
146 | : "Add songs to your playlist!",
147 | status: "error",
148 | });
149 | } else {
150 | createPlaylist();
151 | }
152 | };
153 |
154 | return (
155 |
161 |
162 |
163 | Create a Playlist
164 |
165 |
175 |
176 |
177 |
178 |
179 |
180 | Private playlist?
181 |
182 |
188 | setInputs({ ...inputs, isPrivate: !inputs.isPrivate })
189 | }
190 | />
191 |
192 |
193 |
194 |
198 |
199 |
200 | Playlist Name
201 |
202 |
212 | setInputs({ ...inputs, playlistName: e.target.value })
213 | }
214 | />
215 |
216 |
217 |
218 | Playlist Description (optional)
219 |
220 |
230 | setInputs({ ...inputs, playlistDesc: e.target.value })
231 | }
232 | />
233 |
234 |
235 |
236 |
237 | Add Songs
238 |
239 | {favoritesLoading ? (
240 |
246 |
247 |
248 | ) : error.favorites ? (
249 |
250 | Sorry, an error occured
251 |
252 | ) : (
253 | <>
254 |
255 | From Your favorites
256 |
257 |
258 |
262 | {favorites?.map((song) => (
263 | toggleAddSong(song?._id)}
267 | isAdded={songIsInPlaylist(song?._id)}
268 | />
269 | ))}
270 |
271 | >
272 | )}
273 |
274 |
275 | From Library
276 |
277 |
289 |
290 |
291 | {otherSongsLoading ? (
292 |
298 |
299 |
300 | ) : error.otherSongs ? (
301 |
302 | Sorry, an error occured
303 |
304 | ) : (
305 | <>
306 |
310 | {otherSongs?.map((song) => (
311 | toggleAddSong(song?._id)}
316 | />
317 | ))}
318 |
319 | >
320 | )}
321 |
322 |
323 |
324 | );
325 | };
326 |
327 | export default CreatePlaylistPage;
328 |
--------------------------------------------------------------------------------