├── .gitignore
├── README.md
├── client
├── .eslintrc.cjs
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
│ ├── AlbumLogo.png
│ ├── AuthIMG.png
│ ├── LandingPageIMG.png
│ ├── TunesLogo.png
│ ├── TunesProfileBg.jpeg
│ ├── alternateMusic.jpeg
│ ├── dance.jpeg
│ ├── hiphop.jpeg
│ ├── index.html
│ ├── pop.jpeg
│ ├── rnb.jpeg
│ ├── rock.jpeg
│ └── sitemap.xml
├── src
│ ├── About.jsx
│ ├── App.jsx
│ ├── Artist.jsx
│ ├── Artists.jsx
│ ├── Charts.jsx
│ ├── Explore.jsx
│ ├── Genre.jsx
│ ├── Genres.jsx
│ ├── Home.jsx
│ ├── LandingPage.jsx
│ ├── Login.jsx
│ ├── NotFound.jsx
│ ├── SearchHome.jsx
│ ├── Signup.jsx
│ ├── Theme.jsx
│ ├── assets
│ │ └── components
│ │ │ ├── AppNavbar.jsx
│ │ │ ├── ArtistCard.jsx
│ │ │ ├── ChartCard.jsx
│ │ │ ├── ControlledArtistsCarousel.jsx
│ │ │ ├── ControlledChartsCarousel.jsx
│ │ │ ├── ControlledGenresCarousel.jsx
│ │ │ ├── FavCard.jsx
│ │ │ ├── FavouriteSongs.jsx
│ │ │ ├── FilterArtists.jsx
│ │ │ ├── FilterCharts.jsx
│ │ │ ├── FilterGenres.jsx
│ │ │ ├── Footer.jsx
│ │ │ ├── GenreCard.jsx
│ │ │ ├── LikedSongs.jsx
│ │ │ ├── Profile.jsx
│ │ │ ├── SearchFavSongs.jsx
│ │ │ ├── SearchLikedSongs.jsx
│ │ │ ├── SearchProfile.jsx
│ │ │ ├── SongCard.jsx
│ │ │ └── UserNotFound.jsx
│ ├── index.css
│ └── main.jsx
├── vercel.json
└── vite.config.js
├── package-lock.json
├── package.json
└── server
├── index.js
├── models
├── Employee.js
├── artist.js
├── genre.js
└── song.js
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node.js dependencies
2 | node_modules/
3 | client/node_modules/
4 | server/node_modules/
5 |
6 | # Logs
7 | logs/
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 |
13 | # Runtime data
14 | pids/
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Optional npm cache directory
20 | .npm
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov/
24 |
25 | # Coverage directory used by testing tools like istanbul
26 | coverage/
27 | server/coverage/
28 | client/coverage/
29 |
30 | # Production build
31 | client/build/
32 | client/dist/
33 | server/dist/
34 |
35 | # Miscellaneous
36 | .DS_Store
37 | *.pem
38 |
39 | # Environment variables
40 | .env
41 | client/.env
42 | server/.env
43 |
44 | # Vite files (client-side bundling)
45 | .vite
46 |
47 | # Tailwind generated files
48 | tailwind.config.js
49 |
50 | # Ignore any local IDE config files (optional)
51 | .vscode/
52 | .idea/
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TunesApp
2 | Visit: https://tunes-music-app.vercel.app/
3 |
--------------------------------------------------------------------------------
/client/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
15 |
16 |
17 | Tunes
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@chakra-ui/icons": "^2.1.1",
14 | "@chakra-ui/react": "^2.8.2",
15 | "@emotion/react": "^11.13.0",
16 | "@emotion/styled": "^11.13.0",
17 | "axios": "^1.7.2",
18 | "bootstrap": "^5.3.3",
19 | "framer-motion": "^11.3.21",
20 | "react": "^18.3.1",
21 | "react-bootstrap": "^2.10.4",
22 | "react-dom": "^18.3.1",
23 | "react-icons": "^5.2.1",
24 | "react-router-dom": "^6.24.1",
25 | "react-slick": "^0.30.2",
26 | "slick-carousel": "^1.8.1"
27 | },
28 | "devDependencies": {
29 | "@types/react": "^18.3.3",
30 | "@types/react-dom": "^18.3.0",
31 | "@vitejs/plugin-react": "^4.3.1",
32 | "eslint": "^8.57.0",
33 | "eslint-plugin-react": "^7.34.2",
34 | "eslint-plugin-react-hooks": "^4.6.2",
35 | "eslint-plugin-react-refresh": "^0.4.7",
36 | "tailwindcss": "^3.4.7",
37 | "vite": "^5.3.1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/client/public/AlbumLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/AlbumLogo.png
--------------------------------------------------------------------------------
/client/public/AuthIMG.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/AuthIMG.png
--------------------------------------------------------------------------------
/client/public/LandingPageIMG.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/LandingPageIMG.png
--------------------------------------------------------------------------------
/client/public/TunesLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/TunesLogo.png
--------------------------------------------------------------------------------
/client/public/TunesProfileBg.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/TunesProfileBg.jpeg
--------------------------------------------------------------------------------
/client/public/alternateMusic.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/alternateMusic.jpeg
--------------------------------------------------------------------------------
/client/public/dance.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/dance.jpeg
--------------------------------------------------------------------------------
/client/public/hiphop.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/hiphop.jpeg
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Your App Title
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/client/public/pop.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/pop.jpeg
--------------------------------------------------------------------------------
/client/public/rnb.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/rnb.jpeg
--------------------------------------------------------------------------------
/client/public/rock.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/public/rock.jpeg
--------------------------------------------------------------------------------
/client/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | https://tunes-music-app.vercel.app/
6 | 2025-04-06T13:19:45+01:00
7 | 1.0
8 |
9 |
10 | https://tunes-music-app.vercel.app/register
11 | 2025-04-06T13:19:45+01:00
12 | 1.0
13 |
14 |
15 | https://tunes-music-app.vercel.app/login
16 | 2025-04-06T13:19:45+01:00
17 | 1.0
18 |
19 |
20 | https://tunes-music-app.vercel.app/about
21 | 2025-04-06T13:19:45+01:00
22 | 1.0
23 |
24 |
25 | https://tunes-music-app.vercel.app/explore
26 | 2025-04-06T13:19:45+01:00
27 | 1.0
28 |
29 |
30 | https://tunes-music-app.vercel.app/genres
31 | 2025-04-06T13:19:45+01:00
32 | 1.0
33 |
34 |
35 | https://tunes-music-app.vercel.app/artists
36 | 2025-04-06T13:19:45+01:00
37 | 1.0
38 |
39 |
40 | https://tunes-music-app.vercel.app/artistprofile
41 | 2025-04-06T13:19:45+01:00
42 | 1.0
43 |
44 |
45 | https://tunes-music-app.vercel.app/charts
46 | 2025-04-06T13:19:45+01:00
47 | 1.0
48 |
49 |
50 | https://tunes-music-app.vercel.app/unf
51 | 2025-04-06T13:19:45+01:00
52 | 1.0
53 |
54 |
--------------------------------------------------------------------------------
/client/src/About.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Box,
4 | Container,
5 | Heading,
6 | Text,
7 | VStack,
8 | HStack,
9 | Link,
10 | Center,
11 | useColorModeValue,
12 | Icon,
13 | } from "@chakra-ui/react";
14 | import { FaGithub, FaLinkedin, FaInstagram } from "react-icons/fa";
15 | import AppNavbar from "./assets/components/AppNavbar";
16 |
17 | const About = () => {
18 | const cardBg = useColorModeValue("white", "gray.800");
19 | const textColor = useColorModeValue("gray.800", "white");
20 | const iconColor = useColorModeValue("gray.600", "gray.300");
21 |
22 | return (
23 | <>
24 |
25 |
31 |
32 |
33 |
41 |
42 |
43 | About This Website
44 |
45 |
46 | This web app is a platform where users can log in, upload
47 | their favorite songs, and search for friends' accounts. Users
48 | can like their friends' favorite songs, explore music through
49 | various genres, view the top charts, and listen to their
50 | favorite artists. Built using modern web technologies, it
51 | provides a seamless and interactive experience for music
52 | enthusiasts.
53 |
54 |
55 | Technologies Used: React, Node.js, Express,
56 | MongoDB, React-Bootstrap, Chakra UI
57 |
58 |
63 |
68 | ~Amithkumar P Radhakrishnan
69 |
70 |
71 |
72 |
73 |
74 |
78 |
79 |
80 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | >
94 | );
95 | };
96 |
97 | export default About;
98 |
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Signup from "./Signup";
3 | import "bootstrap/dist/css/bootstrap.min.css";
4 | import { BrowserRouter, Routes, Route } from "react-router-dom";
5 | import Login from "./Login";
6 | import Home from "./Home";
7 | import NotFound from "./NotFound";
8 | import LandingPage from "./LandingPage";
9 | import SearchHome from "./SearchHome";
10 | import About from "./About";
11 | import UserNotFound from "./assets/components/UserNotFound";
12 | import Explore from "./Explore";
13 | import Genres from "./Genres";
14 | import Genre from "./Genre";
15 | import Artist from "./Artist";
16 | import Artists from "./Artists";
17 | import Charts from "./Charts";
18 |
19 | const App = () => {
20 | return (
21 |
22 |
23 | } />
24 | } />
25 | } />
26 | } />
27 | } />
28 | } />
29 | } />
30 | } />
31 | } />
32 | } />
33 | } />
34 | } />
35 | } />
36 | } />
37 |
38 |
39 | );
40 | };
41 |
42 | export default App;
43 |
--------------------------------------------------------------------------------
/client/src/Artist.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import AppNavbar from "./assets/components/AppNavbar";
3 | import { Box } from "@chakra-ui/react";
4 | import Footer from "./assets/components/Footer";
5 |
6 | const Artist = () => {
7 | return (
8 | <>
9 |
10 |
11 |
12 |
13 | >
14 | );
15 | };
16 |
17 | export default Artist;
18 |
--------------------------------------------------------------------------------
/client/src/Artists.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import FilterArtists from "./assets/components/FilterArtists";
3 | import AppNavbar from "./assets/components/AppNavbar";
4 | import Footer from "./assets/components/Footer";
5 | import { Box } from "@chakra-ui/react";
6 |
7 | const Artists = () => {
8 | return (
9 | <>
10 |
16 |
17 |
18 |
19 |
20 | >
21 | );
22 | };
23 |
24 | export default Artists;
25 |
--------------------------------------------------------------------------------
/client/src/Charts.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import FilterCharts from "./assets/components/FilterCharts";
3 | import AppNavbar from "./assets/components/AppNavbar";
4 | import Footer from "./assets/components/Footer";
5 | import { Box } from "@chakra-ui/react";
6 | import Genre from "./Genre";
7 |
8 | const Charts = () => {
9 | return (
10 | <>
11 |
17 |
18 |
19 |
20 |
21 | >
22 | );
23 | };
24 |
25 | export default Charts;
26 |
--------------------------------------------------------------------------------
/client/src/Explore.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Box,
4 | Container,
5 | Heading,
6 | Text,
7 | VStack,
8 | HStack,
9 | Link,
10 | Center,
11 | useColorModeValue,
12 | Icon,
13 | } from "@chakra-ui/react";
14 | import { FaGithub, FaLinkedin, FaInstagram } from "react-icons/fa";
15 | import AppNavbar from "./assets/components/AppNavbar";
16 |
17 | import Genres from "./Genres";
18 | import FilteredGenres from "./assets/components/FilterGenres";
19 | import FilterCharts from "./assets/components/FilterCharts";
20 | import Footer from "./assets/components/Footer";
21 | import FilterArtists from "./assets/components/FilterArtists";
22 |
23 | const Explore = () => {
24 | return (
25 | <>
26 |
27 |
33 |
34 |
35 | {/* */}
36 |
37 |
38 | >
39 | );
40 | };
41 |
42 | export default Explore;
43 |
--------------------------------------------------------------------------------
/client/src/Genre.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useParams, useNavigate } from "react-router-dom"; // Import useNavigate
3 | import SongCard from "./assets/components/SongCard";
4 | import AppNavbar from "./assets/components/AppNavbar";
5 | import Footer from "./assets/components/Footer";
6 | import {
7 | Box,
8 | Text,
9 | SimpleGrid,
10 | Skeleton,
11 | SkeletonText,
12 | } from "@chakra-ui/react"; // Add Skeleton and SkeletonText
13 |
14 | const Genre = () => {
15 | const { genreName } = useParams(); // Get the genre name from the URL
16 | const [songs, setSongs] = useState([]);
17 | const [liked, setLiked] = useState([]); // Track liked state per song
18 | const [isLoading, setIsLoading] = useState(true); // Add loading state
19 | const navigate = useNavigate(); // For redirection
20 |
21 | // Map of genres to their corresponding gradient backgrounds
22 | const genreGradients = {
23 | pop: "linear(to-b, orange.600, yellow.300)",
24 | "rap-hip-hop": "linear(to-b, gray.700, red.400)",
25 | rock: "linear(to-b, blue.900, gray.200)",
26 | dance: "linear(to-b, red.500, yellow.300)",
27 | "r&b": "linear(to-b, purple.900, blue.300)",
28 | alternative: "linear(to-b, purple.900, pink.400)",
29 | };
30 |
31 | const genreDisplayName = genreName
32 | .split("-") // Handle genre names like "hip-hop"
33 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
34 | .join(" ");
35 |
36 | useEffect(() => {
37 | const fetchSongs = async () => {
38 | setIsLoading(true); // Set loading to true when fetching starts
39 | try {
40 | const response = await fetch(
41 | `https://tunesmusicapp.onrender.com/songs?genre=${genreName}`
42 | );
43 | const data = await response.json();
44 | setSongs(data); // Adjust according to your API response structure
45 |
46 | // Initialize liked state for all songs to 'false'
47 | setLiked(new Array(data.length).fill(false));
48 | } catch (error) {
49 | console.error("Error fetching songs:", error);
50 | } finally {
51 | setIsLoading(false); // Set loading to false after fetching
52 | }
53 | };
54 |
55 | fetchSongs();
56 | }, [genreName]);
57 |
58 | // Handle liking/unliking a song
59 | const handleLike = async (index) => {
60 | const song = songs[index];
61 | const isLiked = liked[index];
62 |
63 | try {
64 | let response;
65 | if (isLiked) {
66 | // Unlike the song
67 | response = await fetch(
68 | "https://tunesmusicapp.onrender.com/deleteLikedSong",
69 | {
70 | method: "DELETE",
71 | headers: {
72 | "Content-Type": "application/json",
73 | },
74 | credentials: "include",
75 | body: JSON.stringify({
76 | songId: song._id, // Use the song's unique ID
77 | }),
78 | }
79 | );
80 | } else {
81 | // Like the song
82 | response = await fetch("https://tunesmusicapp.onrender.com/likedSong", {
83 | method: "POST",
84 | headers: {
85 | "Content-Type": "application/json",
86 | },
87 | credentials: "include",
88 | body: JSON.stringify({
89 | songName: song.title,
90 | songLink: song.link,
91 | }),
92 | });
93 | }
94 |
95 | // Check if the response is a 403 Forbidden
96 | if (response.status === 403) {
97 | alert("Please login to like a song.");
98 | navigate("/login"); // Redirect to login page
99 | } else {
100 | // Update local liked state after the API call
101 | const newLiked = [...liked];
102 | newLiked[index] = !newLiked[index];
103 | setLiked(newLiked);
104 | }
105 | } catch (err) {
106 | console.error("Error toggling like for song", err);
107 | }
108 | };
109 |
110 | return (
111 | <>
112 |
113 |
114 |
115 | {/* Genre card with background gradient */}
116 |
132 |
142 | {genreDisplayName} Genre
143 |
144 |
145 |
146 | {/* Song cards */}
147 |
148 |
149 | {isLoading ? (
150 | // Display skeletons while loading
151 | Array.from({ length: 6 }).map((_, index) => (
152 |
153 |
154 |
155 | ))
156 | ) : songs.length > 0 ? (
157 | // Display the song cards once the data is loaded
158 | songs.map((song, index) => (
159 | handleLike(index)}
164 | />
165 | ))
166 | ) : (
167 |
168 | No songs available for this genre.
169 |
170 | )}
171 |
172 |
173 |
174 |
175 | >
176 | );
177 | };
178 |
179 | export default Genre;
180 |
--------------------------------------------------------------------------------
/client/src/Genres.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import GenreCard from "./assets/components/GenreCard";
3 | import {
4 | Box,
5 | SimpleGrid,
6 | Text,
7 | Skeleton,
8 | SkeletonText,
9 | } from "@chakra-ui/react";
10 | import AppNavbar from "./assets/components/AppNavbar";
11 | import Footer from "./assets/components/Footer";
12 | import { useNavigate } from "react-router-dom"; // Import useNavigate
13 |
14 | const Genres = () => {
15 | const [genres, setGenres] = useState([]);
16 | const [isLoading, setIsLoading] = useState(true); // Loading state
17 | const navigate = useNavigate(); // Initialize navigate
18 |
19 | const coverImages = [
20 | "./pop.jpeg",
21 | "./hiphop.jpeg",
22 | "./rock.jpeg",
23 | "./dance.jpeg",
24 | "./rnb.jpeg",
25 | "./alternateMusic.jpeg",
26 | "./electro.jpeg",
27 | "./folk.jpeg",
28 | "./reggae.jpeg",
29 | "./jazz.jpeg",
30 | "./classical.jpeg",
31 | ];
32 |
33 | useEffect(() => {
34 | const fetchGenres = async () => {
35 | try {
36 | const response = await fetch(
37 | "https://tunesmusicapp.onrender.com/genres"
38 | );
39 |
40 | const data = await response.json();
41 | console.log(data);
42 | setGenres(data.data); // Fetch only the first 12 genres
43 | } catch (error) {
44 | console.error("Error fetching genres:", error);
45 | } finally {
46 | setIsLoading(false); // Set loading to false after fetching
47 | }
48 | };
49 |
50 | fetchGenres();
51 | }, []);
52 |
53 | const handleClick = (genre) => {
54 | const genreName = genre.name
55 | .toLowerCase()
56 | .replace(/\s+/g, "-")
57 | .replace(/\//g, "-");
58 | navigate(`/genres/${genreName}`);
59 | };
60 |
61 | return (
62 |
63 |
64 |
71 | Explore Genres
72 |
73 |
80 | {isLoading
81 | ? Array.from({ length: 6 }).map((_, index) => (
82 |
83 |
84 |
85 | ))
86 | : genres.map((genre, index) => (
87 | handleClick(genre)} // Pass the genre to handleClick
89 | key={genre.id}
90 | genre={genre}
91 | coverImage={coverImages[index]}
92 | />
93 | ))}
94 |
95 |
96 |
97 | );
98 | };
99 |
100 | export default Genres;
101 |
--------------------------------------------------------------------------------
/client/src/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useNavigate, useParams } from "react-router-dom";
3 | import axios from "axios";
4 | import { Box, Container, Spinner } from "@chakra-ui/react";
5 | import AppNavbar from "./assets/components/AppNavbar";
6 | import Profile from "./assets/components/Profile";
7 | import FavouriteSongs from "./assets/components/FavouriteSongs";
8 | import LikedSongs from "./assets/components/LikedSongs";
9 | import Footer from "./assets/components/Footer";
10 |
11 | const Home = () => {
12 | const navigate = useNavigate();
13 | const [authStatus, setAuthStatus] = useState(null);
14 |
15 | axios.defaults.withCredentials = true;
16 |
17 | useEffect(() => {
18 | axios
19 | .get("https://tunesmusicapp.onrender.com/", { withCredentials: true })
20 | .then((result) => {
21 | if (result.data === "Success") {
22 | setAuthStatus("Authenticated");
23 | } else {
24 | setAuthStatus("Not authenticated");
25 | navigate("/login");
26 | }
27 | })
28 | .catch((err) => {
29 | console.error(err);
30 | setAuthStatus("Error");
31 | navigate("/login");
32 | });
33 | }, [navigate]);
34 |
35 | if (authStatus === null) {
36 | return (
37 |
44 |
45 |
46 | );
47 | }
48 |
49 | return (
50 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default Home;
68 |
--------------------------------------------------------------------------------
/client/src/LandingPage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Box,
4 | Button,
5 | Container,
6 | Flex,
7 | Heading,
8 | HStack,
9 | Image,
10 | Text,
11 | VStack,
12 | } from "@chakra-ui/react";
13 | import { useNavigate } from "react-router-dom";
14 | import { ArrowForwardIcon } from "@chakra-ui/icons";
15 |
16 | const LandingPage = () => {
17 | const navigate = useNavigate();
18 | const handleClick = () => {
19 | navigate("/login");
20 | };
21 | return (
22 |
30 |
31 |
37 | {/* Left Content */}
38 |
43 |
44 |
50 | TUNES
51 |
52 |
58 |
59 |
60 | Discover, Stream, and Enjoy Unlimited Music
61 |
62 |
68 | Experience music like never before. Explore millions of tracks,
69 | create your perfect favlists, and connect with artists worldwide.
70 |
71 | }
77 | textColor="white"
78 | onClick={handleClick}
79 | >
80 | Login to TUNES
81 |
82 |
83 |
84 | {/* Right Content - Placeholder Image/Graphic */}
85 |
90 |
97 |
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | export default LandingPage;
105 |
--------------------------------------------------------------------------------
/client/src/Login.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import axios from "axios";
3 | import { useNavigate } from "react-router-dom";
4 | import {
5 | Box,
6 | Container,
7 | Flex,
8 | VStack,
9 | Heading,
10 | FormControl,
11 | FormLabel,
12 | Input,
13 | Button,
14 | Text,
15 | Image,
16 | Alert,
17 | AlertIcon,
18 | Spinner,
19 | } from "@chakra-ui/react";
20 |
21 | const Login = () => {
22 | const [email, setEmail] = useState("");
23 | const [password, setPassword] = useState("");
24 | const [errorMessage, setErrorMessage] = useState("");
25 | const [loading, setLoading] = useState(false); // Loading state
26 | const navigate = useNavigate();
27 |
28 | axios.defaults.withCredentials = true;
29 |
30 | const handleSubmit = (e) => {
31 | e.preventDefault();
32 | setErrorMessage("");
33 | setLoading(true); // Start loading
34 |
35 | const baseURL = "https://tunesmusicapp.onrender.com";
36 | // const baseURL = "http://localhost:3000";
37 |
38 | axios
39 | .post(`${baseURL}/login`, { email, password })
40 | .then((result) => {
41 | setLoading(false); // Stop loading
42 | if (result.data.message === "Success") {
43 | console.log("Login in Successful");
44 | navigate(`/${result.data.email}`);
45 | }
46 | })
47 | .catch((err) => {
48 | setLoading(false); // Stop loading
49 | if (err.response) {
50 | if (err.response.status === 401) {
51 | setErrorMessage("Incorrect password");
52 | } else if (err.response.status === 404) {
53 | setErrorMessage("User not found. Redirecting to Signup Page...");
54 | setTimeout(() => {
55 | navigate("/register");
56 | }, 5000);
57 | } else {
58 | setErrorMessage(err.response.data);
59 | }
60 | } else if (err.request) {
61 | setErrorMessage("No response from the server. Please try again.");
62 | } else {
63 | setErrorMessage("An error occurred. Please try again.");
64 | }
65 | });
66 | };
67 |
68 | return (
69 | <>
70 |
78 |
79 |
80 | {/* Left Side */}
81 |
88 |
96 |
97 |
98 | {/* Right Side - Login Form */}
99 |
113 |
114 |
121 | TUNES
122 |
123 |
124 |
125 |
126 | {errorMessage && (
127 |
128 |
129 | {errorMessage}
130 |
131 | )}
132 |
133 |
189 |
190 |
191 |
192 |
193 | >
194 | );
195 | };
196 |
197 | export default Login;
198 |
--------------------------------------------------------------------------------
/client/src/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, Text, VStack, Box } from "@chakra-ui/react";
3 | import { useNavigate } from "react-router-dom";
4 |
5 | const NotFound = () => {
6 | const navigate = useNavigate();
7 |
8 | const handleLogin = () => {
9 | navigate("/login");
10 | };
11 |
12 | return (
13 |
20 |
21 |
22 | 404
23 |
24 |
25 | Sorry, the page you are looking for does not exist.
26 |
27 |
37 | Login to TUNES
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default NotFound;
45 |
--------------------------------------------------------------------------------
/client/src/SearchHome.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Container } from "@chakra-ui/react";
3 | import SearchProfile from "./assets/components/SearchProfile";
4 | import SearchFavSongs from "./assets/components/SearchFavSongs";
5 | import Footer from "./assets/components/Footer";
6 | import SearchLikedSongs from "./assets/components/SearchLikedSongs";
7 |
8 | const SearchHome = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default SearchHome;
22 |
--------------------------------------------------------------------------------
/client/src/Signup.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Link, useNavigate } from "react-router-dom";
3 | import {
4 | Box,
5 | Container,
6 | Flex,
7 | VStack,
8 | Heading,
9 | FormControl,
10 | FormLabel,
11 | Input,
12 | Button,
13 | Text,
14 | Image,
15 | Alert,
16 | AlertIcon,
17 | Spinner,
18 | useBreakpointValue,
19 | } from "@chakra-ui/react";
20 | import axios from "axios";
21 |
22 | const Signup = () => {
23 | const [name, setName] = useState("");
24 | const [email, setEmail] = useState("");
25 | const [password, setPassword] = useState("");
26 | const [errorMessage, setErrorMessage] = useState("");
27 | const [isLoading, setIsLoading] = useState(false);
28 | const navigate = useNavigate();
29 |
30 | const handleSubmit = (e) => {
31 | e.preventDefault();
32 | setIsLoading(true);
33 | setErrorMessage("");
34 |
35 | const baseURL = "https://tunesmusicapp.onrender.com";
36 | // const baseURL = "http://localhost:3000";
37 |
38 | axios
39 | .post(`${baseURL}/register`, { name, email, password })
40 | .then((result) => {
41 | setIsLoading(false);
42 | if (result.data === "Duplicate Email") {
43 | setErrorMessage(
44 | "This email ID already has an account. Redirecting to Login Page..."
45 | );
46 | setTimeout(() => {
47 | navigate("/login");
48 | }, 5000);
49 | } else {
50 | navigate("/login");
51 | }
52 | })
53 | .catch((err) => {
54 | setIsLoading(false);
55 | if (err.response && err.response.data === "Duplicate Email") {
56 | setErrorMessage(
57 | "This email ID already has an account. Redirecting to Login Page..."
58 | );
59 | setTimeout(() => {
60 | navigate("/login");
61 | }, 5000);
62 | } else {
63 | setErrorMessage("An error occurred. Please try again.");
64 | }
65 | });
66 | };
67 |
68 | return (
69 | <>
70 |
78 |
79 |
84 | {/* Left Side */}
85 |
92 |
99 |
100 |
101 | {/* Right Side - Signup Form */}
102 |
115 |
116 |
123 | TUNES
124 |
125 |
126 |
127 |
128 | {errorMessage && (
129 |
130 |
131 | {errorMessage}
132 |
133 | )}
134 |
198 |
199 |
200 |
201 |
202 | >
203 | );
204 | };
205 |
206 | export default Signup;
207 |
--------------------------------------------------------------------------------
/client/src/Theme.jsx:
--------------------------------------------------------------------------------
1 | import { extendTheme } from "@chakra-ui/react";
2 |
3 | const theme = extendTheme({
4 | styles: {
5 | global: {
6 | // Disable horizontal scrolling
7 | "@media (max-width: 768px)": {
8 | body: {
9 | overflowX: "hidden",
10 | },
11 | },
12 | // Custom scrollbar styles
13 | "::-webkit-scrollbar": {
14 | width: "8px",
15 | height: "4px",
16 | },
17 | "::-webkit-scrollbar-thumb": {
18 | background: "#4A5568", // Scrollbar thumb color
19 | borderRadius: "10px",
20 | border: "2px solid transparent",
21 | backgroundClip: "content-box",
22 | },
23 | "::-webkit-scrollbar-thumb:hover": {
24 | background: "#718096", // Hover state for scrollbar thumb
25 | },
26 | "::-webkit-scrollbar-track": {
27 | background: "#2D3748", // Scrollbar track color
28 | borderRadius: "10px",
29 | },
30 | },
31 | },
32 | });
33 |
34 | export default theme;
35 |
--------------------------------------------------------------------------------
/client/src/assets/components/AppNavbar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import axios from "axios";
3 | import { useNavigate, useLocation } from "react-router-dom";
4 | import {
5 | Box,
6 | Flex,
7 | Button,
8 | Input,
9 | InputGroup,
10 | InputRightElement,
11 | Image,
12 | Heading,
13 | useDisclosure,
14 | Drawer,
15 | DrawerBody,
16 | DrawerFooter,
17 | DrawerHeader,
18 | DrawerOverlay,
19 | DrawerContent,
20 | DrawerCloseButton,
21 | IconButton,
22 | VStack,
23 | HStack,
24 | Link,
25 | List,
26 | ListItem,
27 | ListIcon,
28 | } from "@chakra-ui/react";
29 | import { HamburgerIcon } from "@chakra-ui/icons";
30 |
31 | const AppNavbar = () => {
32 | const [email, setEmail] = useState("");
33 | const [searchQuery, setSearchQuery] = useState("");
34 | const [suggestions, setSuggestions] = useState([]);
35 | const { isOpen, onOpen, onClose } = useDisclosure();
36 | const navigate = useNavigate();
37 | const location = useLocation();
38 | const isAuthPage =
39 | location.pathname === "/login" || location.pathname === "/register";
40 |
41 | useEffect(() => {
42 | const fetchUserData = async () => {
43 | try {
44 | const response = await fetch(
45 | "https://tunesmusicapp.onrender.com/user",
46 | {
47 | credentials: "include",
48 | }
49 | );
50 | const data = await response.json();
51 | setEmail(data.email);
52 | } catch (error) {
53 | console.error("Error fetching user data:", error);
54 | }
55 | };
56 |
57 | fetchUserData();
58 | }, []);
59 |
60 | const navigateHome = () => {
61 | navigate(`/${email}`);
62 | };
63 |
64 | const navigateAbout = () => {
65 | navigate("/about");
66 | };
67 |
68 | const navigateExplore = () => {
69 | navigate("/explore");
70 | };
71 |
72 | const handleSearch = (e) => {
73 | e.preventDefault();
74 | axios
75 | .post(
76 | "https://tunesmusicapp.onrender.com/searchUser",
77 | { query: searchQuery },
78 | { withCredentials: true }
79 | )
80 | .then((response) => {
81 | navigate(`/${searchQuery}/search`);
82 | })
83 | .catch((err) => {
84 | if (err.response && err.response.status === 404) {
85 | navigate("/unf");
86 | } else if (err.response && err.response.status === 403) {
87 | navigate("/");
88 | } else {
89 | console.error(err);
90 | }
91 | });
92 | };
93 |
94 | const handleLogout = () => {
95 | axios
96 | .get("https://tunesmusicapp.onrender.com/logout", {
97 | withCredentials: true,
98 | })
99 | .then(() => {
100 | navigate("/login");
101 | })
102 | .catch((err) => console.log(err));
103 | };
104 |
105 | const fetchSuggestions = async (query) => {
106 | if (!query) {
107 | setSuggestions([]);
108 | return;
109 | }
110 | try {
111 | const response = await axios.post(
112 | "https://tunesmusicapp.onrender.com/suggestUsers",
113 | { query },
114 | { withCredentials: true }
115 | );
116 | console.log("Suggestions response:", response.data);
117 | setSuggestions(response.data);
118 | } catch (error) {
119 | console.error("Error fetching suggestions:", error);
120 | }
121 | };
122 |
123 | return (
124 |
132 |
139 |
140 |
141 | TUNES
142 |
143 |
148 |
149 |
150 | {!isAuthPage && (
151 |
152 |
162 | Home
163 |
164 |
165 |
175 | Explore
176 |
177 |
178 |
188 | About Us
189 |
190 |
191 | )}
192 |
193 |
194 | {!isAuthPage && (
195 |
201 |
202 | {
207 | setSearchQuery(e.target.value);
208 | fetchSuggestions(e.target.value); // Fetch suggestions as the user types
209 | }}
210 | color="white"
211 | bg="gray.700"
212 | border="none"
213 | _placeholder={{ color: "gray.400" }}
214 | _hover={{ bg: "gray.600" }}
215 | _focus={{ bg: "gray.600" }}
216 | />
217 |
218 |
226 | Search
227 |
228 |
229 |
230 | {/* Suggestions List */}
231 | {suggestions.length > 0 && (
232 |
253 | {suggestions.map((user) => (
254 | {
259 | setSearchQuery(user.name); // Set the search query to the selected suggestion
260 | setSuggestions([]); // Clear suggestions
261 | }}
262 | >
263 |
264 | {user.name}
265 |
266 | ))}
267 |
268 | )}
269 |
276 | Logout
277 |
278 |
279 | )}
280 |
281 | }
286 | onClick={onOpen}
287 | />
288 |
289 |
290 |
291 |
292 | Menu
293 |
294 |
295 | {!isAuthPage && (
296 | <>
297 |
306 | Home
307 |
308 |
309 |
318 | Explore
319 |
320 |
321 |
330 | About Us
331 |
332 |
333 |
334 |
335 | {
339 | setSearchQuery(e.target.value);
340 | fetchSuggestions(e.target.value); // Fetch suggestions as the user types
341 | }}
342 | color="white"
343 | bg="gray.700"
344 | border="none"
345 | _placeholder={{ color: "gray.400" }}
346 | _hover={{ bg: "gray.600" }}
347 | _focus={{ bg: "gray.600" }}
348 | />
349 |
350 |
358 | Search
359 |
360 |
361 |
362 |
368 | Logout
369 |
370 | {/* Suggestions List */}
371 | {suggestions.length > 0 && (
372 |
382 | {suggestions.map((user) => (
383 | {
388 | setSearchQuery(user.name); // Set the search query to the selected suggestion
389 | setSuggestions([]); // Clear suggestions
390 | }}
391 | >
392 |
393 | {user.name}
394 |
395 | ))}
396 |
397 | )}
398 |
399 | >
400 | )}
401 |
402 |
403 |
404 |
405 | Close
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 | );
414 | };
415 |
416 | export default AppNavbar;
417 |
--------------------------------------------------------------------------------
/client/src/assets/components/ArtistCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Box, Image, Text, IconButton } from "@chakra-ui/react";
3 | import { FaPlay } from "react-icons/fa";
4 | import { useNavigate } from "react-router-dom";
5 |
6 | const ArtistCard = ({ artistName, coverImage, songLink, isMobile }) => {
7 | const [isHovered, setIsHovered] = useState(false);
8 | const navigate = useNavigate();
9 |
10 | return (
11 | !isMobile && setIsHovered(true)} // Set hover state only if not mobile
15 | onMouseLeave={() => !isMobile && setIsHovered(false)} // Remove hover state only if not mobile
16 | _hover={{ backgroundColor: "gray.800" }}
17 | >
18 |
29 |
37 |
38 | {/* Always show play button on mobile, otherwise show on hover */}
39 | }
42 | rounded="full"
43 | boxSize={14}
44 | bg="#1DB954"
45 | position="absolute"
46 | top="67%"
47 | left="63%"
48 | zIndex={10} // Ensure it appears on top of the image
49 | _hover={{ bg: "#1ED760", transform: "scale(1.1)" }}
50 | onClick={() => window.open(songLink, "_blank")}
51 | display={isMobile || isHovered ? "flex" : "none"} // Show button if mobile or hovered
52 | />
53 |
54 |
55 |
56 | {artistName}
57 |
58 | Artist
59 |
60 |
61 | );
62 | };
63 |
64 | export default ArtistCard;
65 |
--------------------------------------------------------------------------------
/client/src/assets/components/ChartCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Image, Text } from "@chakra-ui/react";
3 |
4 | const ChartCard = ({ songTitle, artist, coverImage, onClick }) => {
5 | console.log(songTitle, artist);
6 |
7 | return (
8 |
18 |
26 |
37 | {songTitle}
38 |
39 |
49 | {artist}
50 |
51 |
52 | );
53 | };
54 |
55 | export default ChartCard;
56 |
--------------------------------------------------------------------------------
/client/src/assets/components/ControlledArtistsCarousel.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Carousel from "react-bootstrap/Carousel";
3 | import ArtistCard from "./ArtistCard";
4 |
5 | function ControlledArtistsCarousel({ artists }) {
6 | const [index, setIndex] = useState(0);
7 |
8 | const handleSelect = (selectedIndex) => {
9 | setIndex(selectedIndex);
10 | };
11 |
12 | return (
13 |
14 | {artists.map((artist) => (
15 |
16 |
20 |
21 | ))}
22 |
23 | );
24 | }
25 |
26 | export default ControlledArtistsCarousel;
27 |
--------------------------------------------------------------------------------
/client/src/assets/components/ControlledChartsCarousel.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Carousel from "react-bootstrap/Carousel";
3 | import ChartCard from "./ChartCard";
4 |
5 | function ControlledChartsCarousel({ charts }) {
6 | const [index, setIndex] = useState(0);
7 |
8 | const handleSelect = (selectedIndex) => {
9 | setIndex(selectedIndex);
10 | };
11 |
12 | return (
13 |
14 | {charts.map((chart) => (
15 |
16 | {
21 | window.open(chart.link, "_blank");
22 | }}
23 | />
24 |
25 | ))}
26 |
27 | );
28 | }
29 |
30 | export default ControlledChartsCarousel;
31 |
--------------------------------------------------------------------------------
/client/src/assets/components/ControlledGenresCarousel.jsx:
--------------------------------------------------------------------------------
1 | // import { useState } from "react";
2 | // import Carousel from "react-bootstrap/Carousel";
3 | // import GenreCard from "./GenreCard";
4 |
5 | // function ControlledGenresCarousel({ genres, coverImages, onClick }) {
6 | // const [index, setIndex] = useState(0);
7 |
8 | // const handleSelect = (selectedIndex) => {
9 | // setIndex(selectedIndex);
10 | // };
11 |
12 | // return (
13 | //
14 | // {genres.map((genre, i) => (
15 | //
16 | // onClick(genre)}>
17 | // {" "}
18 | // {/* Pass the onClick handler */}
19 | //
20 | //
21 | //
22 | // ))}
23 | //
24 | // );
25 | // }
26 |
27 | // export default ControlledGenresCarousel;
28 |
29 | import { useState } from "react";
30 | import Carousel from "react-bootstrap/Carousel";
31 | import GenreCard from "./GenreCard";
32 |
33 | function ControlledGenresCarousel({ genres, coverImages, onClick }) {
34 | const [index, setIndex] = useState(0);
35 |
36 | const handleSelect = (selectedIndex) => {
37 | setIndex(selectedIndex);
38 | };
39 |
40 | return (
41 |
42 | {genres.map((genre, i) => (
43 |
44 | onClick(genre)}
48 | />
49 |
50 | ))}
51 |
52 | );
53 | }
54 |
55 | export default ControlledGenresCarousel;
56 |
--------------------------------------------------------------------------------
/client/src/assets/components/FavCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Image } from "@chakra-ui/react";
3 | import { useLocation, useParams } from "react-router-dom";
4 | import {
5 | Box,
6 | Button,
7 | Flex,
8 | Input,
9 | Modal,
10 | ModalBody,
11 | ModalCloseButton,
12 | ModalContent,
13 | ModalFooter,
14 | ModalHeader,
15 | ModalOverlay,
16 | Skeleton,
17 | Text,
18 | FormControl,
19 | FormLabel,
20 | Tooltip,
21 | } from "@chakra-ui/react";
22 | import { AddIcon } from "@chakra-ui/icons";
23 | import { FaPlay, FaMinus } from "react-icons/fa";
24 | import { IconButton } from "@chakra-ui/react";
25 |
26 | const FavCard = () => {
27 | const [songs, setSongs] = useState([]);
28 | const [newSongName, setNewSongName] = useState("");
29 | const [newLink, setNewLink] = useState("");
30 | const [showModal, setShowModal] = useState(false);
31 | const location = useLocation();
32 | const { email } = useParams();
33 | const isSearchPage = location.pathname === `/${email}/search`;
34 |
35 | useEffect(() => {
36 | const fetchUserData = async () => {
37 | try {
38 | const response = await fetch(
39 | "https://tunesmusicapp.onrender.com/user",
40 | {
41 | credentials: "include",
42 | }
43 | );
44 | const data = await response.json();
45 | setSongs(data.favouriteSongs);
46 | } catch (err) {
47 | console.error("Error fetching data", err);
48 | }
49 | };
50 |
51 | fetchUserData();
52 | }, []);
53 |
54 | const handleAdd = () => {
55 | setShowModal(true);
56 | };
57 |
58 | const handleSave = async () => {
59 | try {
60 | const response = await fetch(
61 | "https://tunesmusicapp.onrender.com/addFavouriteSong",
62 | {
63 | method: "POST",
64 | headers: {
65 | "Content-Type": "application/json",
66 | },
67 | credentials: "include",
68 | body: JSON.stringify({
69 | songName: newSongName,
70 | songLink: newLink,
71 | }),
72 | }
73 | );
74 | if (response.ok) {
75 | const newSong = { name: newSongName, link: newLink };
76 | setSongs([...songs, newSong]);
77 | setShowModal(false);
78 | } else {
79 | console.error("Failed to update song details");
80 | }
81 | } catch (error) {
82 | console.error("Error updating song details:", error);
83 | }
84 | };
85 |
86 | const handleDelete = async (index) => {
87 | try {
88 | const songToDelete = songs[index];
89 | const response = await fetch(
90 | "https://tunesmusicapp.onrender.com/deleteFavouriteSong",
91 | {
92 | method: "DELETE",
93 | headers: {
94 | "Content-Type": "application/json",
95 | },
96 | credentials: "include",
97 | body: JSON.stringify({
98 | songId: songToDelete._id, // Ensure that `songId` is used here
99 | }),
100 | }
101 | );
102 | if (response.ok) {
103 | const updatedSongs = songs.filter((_, i) => i !== index);
104 | setSongs(updatedSongs);
105 | } else {
106 | console.error("Failed to delete song");
107 | }
108 | } catch (error) {
109 | console.error("Error deleting song:", error);
110 | }
111 | };
112 |
113 | const [isLoading, setIsLoading] = useState(true);
114 |
115 | useEffect(() => {
116 | const timer = setTimeout(() => {
117 | setIsLoading(false);
118 | }, 2000); // simulate loading time
119 | return () => clearTimeout(timer);
120 | }, []);
121 |
122 | return (
123 |
124 |
125 |
126 | {!isSearchPage && (
127 | <>
128 | {songs.length > 0 ? (
129 |
136 | }
139 | borderRadius="full"
140 | aria-label="Add Songs"
141 | bg="#1DB954"
142 | sx={{
143 | _hover: {
144 | bg: "#1ED760",
145 | transform: "scale(1.1)",
146 | },
147 | }}
148 | boxSize={{ base: "10", md: "10" }}
149 | />
150 |
151 | ) : (
152 |
153 | Add Songs
154 |
155 | )}
156 | >
157 | )}
158 | {isSearchPage && songs.length === 0 && (
159 |
160 | No Songs
161 |
162 | )}
163 |
164 |
165 |
166 | setShowModal(false)}>
167 |
168 |
169 | Add Song Details
170 |
171 |
172 |
173 | Song Name
174 | setNewSongName(e.target.value)}
177 | placeholder="Example: Heat Waves"
178 | />
179 | Song Link
180 | setNewLink(e.target.value)}
183 | placeholder="Example: https://www.youtube.com/your-song-link"
184 | />
185 |
186 |
187 |
188 | setShowModal(false)}
192 | >
193 | Cancel
194 |
195 |
196 | Save
197 |
198 |
199 |
200 |
201 |
202 |
203 | {songs.map((song, index) => (
204 |
212 | {isLoading ? (
213 |
214 | ) : (
215 |
228 |
229 |
236 |
237 | {song.name}
238 |
239 |
240 |
241 |
242 |
249 | }
256 | onClick={() => window.open(song.link, "_blank")}
257 | />
258 |
259 | {!isSearchPage && (
260 |
267 | }
271 | color="red.500"
272 | boxSize={6}
273 | p={1}
274 | onClick={() => handleDelete(index)}
275 | />
276 |
277 | )}
278 |
279 |
280 | )}
281 |
282 | ))}
283 |
284 |
285 | );
286 | };
287 |
288 | export default FavCard;
289 |
--------------------------------------------------------------------------------
/client/src/assets/components/FavouriteSongs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Text, Box } from "@chakra-ui/react";
3 | import {
4 | Container,
5 | Row,
6 | Col,
7 | Image,
8 | Card,
9 | Button,
10 | Modal,
11 | Form,
12 | } from "react-bootstrap";
13 | import { useLocation, useParams } from "react-router-dom";
14 | import FavCard from "./FavCard";
15 |
16 | const FavouriteSongs = () => {
17 | const location = useLocation();
18 | const { email } = useParams();
19 |
20 | return (
21 |
22 |
23 |
24 |
29 | Favourite Songs
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default FavouriteSongs;
40 |
--------------------------------------------------------------------------------
/client/src/assets/components/FilterArtists.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | Box,
4 | SimpleGrid,
5 | Text,
6 | useBreakpointValue,
7 | SkeletonCircle,
8 | Skeleton,
9 | SkeletonText,
10 | } from "@chakra-ui/react";
11 | import ArtistCard from "./ArtistCard";
12 | import ControlledArtistsCarousel from "./ControlledArtistsCarousel";
13 | import { useNavigate, useLocation } from "react-router-dom";
14 |
15 | const FilterArtists = () => {
16 | const [artists, setArtists] = useState([]);
17 | const navigate = useNavigate();
18 | const location = useLocation();
19 | const [isLoading, setIsLoading] = useState(true);
20 | const isMobile = useBreakpointValue({ base: true, md: false }); // Detect screen size
21 |
22 | const handleShowArtists = () => {
23 | navigate("/artists");
24 | };
25 |
26 | useEffect(() => {
27 | const fetchArtists = async () => {
28 | try {
29 | const response = await fetch(
30 | "https://tunesmusicapp.onrender.com/songs"
31 | );
32 | const data = await response.json();
33 | if (location.pathname === "/explore") {
34 | setArtists(data.slice(0, 5)); // Fetch only the first 3 songs on /explore
35 | } else {
36 | setArtists(data); // Fetch all data on other routes
37 | }
38 | } catch (error) {
39 | console.error("Error fetching artists:", error);
40 | } finally {
41 | setIsLoading(false);
42 | }
43 | };
44 |
45 | fetchArtists();
46 | }, [location.pathname]);
47 |
48 | return (
49 |
50 |
57 | Top Artists
58 |
59 | {location.pathname === "/explore" ? (
60 |
68 | Show all
69 |
70 | ) : null}
71 |
72 |
73 | {isMobile ? (
74 | // Display carousel on mobile screens
75 | isLoading ? (
76 | Array.from({ length: 1 }).map((_, index) => (
77 |
78 |
79 |
80 | ))
81 | ) : (
82 |
83 | )
84 | ) : (
85 | // Display grid on larger screens
86 |
87 | {isLoading
88 | ? Array.from({ length: 5 }).map((_, index) => (
89 |
90 |
91 |
92 | ))
93 | : artists.map((artist) => (
94 |
101 | ))}
102 |
103 | )}
104 |
105 |
106 | );
107 | };
108 |
109 | export default FilterArtists;
110 |
--------------------------------------------------------------------------------
/client/src/assets/components/FilterCharts.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | Box,
4 | Text,
5 | Skeleton,
6 | SkeletonText,
7 | useBreakpointValue,
8 | } from "@chakra-ui/react";
9 | import ChartCard from "./ChartCard";
10 | import ControlledChartsCarousel from "./ControlledChartsCarousel"; // Carousel component
11 | import { useNavigate, useLocation } from "react-router-dom";
12 |
13 | const FilterCharts = () => {
14 | const [charts, setCharts] = useState([]);
15 | const [isLoading, setIsLoading] = useState(true);
16 | const navigate = useNavigate();
17 | const location = useLocation(); // Get current route
18 | const isMobile = useBreakpointValue({ base: true, md: false }); // Detect screen size
19 |
20 | const handleShowCharts = () => {
21 | navigate("/charts");
22 | };
23 |
24 | useEffect(() => {
25 | const fetchCharts = async () => {
26 | try {
27 | const response = await fetch(
28 | "https://tunesmusicapp.onrender.com/songs"
29 | );
30 | const data = await response.json();
31 |
32 | // Check if the current route is "/explore"
33 | if (location.pathname === "/explore") {
34 | setCharts(data.slice(0, 3)); // Fetch only the first 3 songs on /explore
35 | } else {
36 | setCharts(data); // Fetch all data on other routes
37 | }
38 | } catch (error) {
39 | console.error("Error fetching charts:", error);
40 | } finally {
41 | setIsLoading(false);
42 | }
43 | };
44 |
45 | fetchCharts();
46 | }, [location.pathname]);
47 |
48 | return (
49 |
50 |
57 | Top Charts
58 |
59 | {location.pathname === "/explore" && (
60 |
68 | Show all
69 |
70 | )}
71 |
72 |
73 | {isMobile ? (
74 | // Display carousel on mobile screens
75 | isLoading ? (
76 | Array.from({ length: 1 }).map((_, index) => (
77 |
78 |
79 |
80 | ))
81 | ) : (
82 | // Pass charts to carousel
83 | )
84 | ) : (
85 | // Display grid on larger screens
86 |
95 | {isLoading
96 | ? Array.from({ length: 3 }).map((_, index) => (
97 |
98 |
99 |
100 | ))
101 | : charts.map((chart) => (
102 | {
108 | window.open(chart.previewLink, "_blank"); // Use previewLink for song link
109 | }}
110 | />
111 | ))}
112 |
113 | )}
114 |
115 |
116 | );
117 | };
118 |
119 | export default FilterCharts;
120 |
--------------------------------------------------------------------------------
/client/src/assets/components/FilterGenres.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | Box,
4 | SimpleGrid,
5 | Text,
6 | useBreakpointValue,
7 | Skeleton,
8 | SkeletonText,
9 | } from "@chakra-ui/react";
10 | import GenreCard from "./GenreCard";
11 | import ControlledGenresCarousel from "./ControlledGenresCarousel";
12 | import { useNavigate } from "react-router-dom";
13 |
14 | const FilteredGenres = () => {
15 | const [genres, setGenres] = useState([]);
16 | const [isLoading, setIsLoading] = useState(true);
17 | const navigate = useNavigate();
18 | const isMobile = useBreakpointValue({ base: true, md: false });
19 |
20 | const handleShowGenre = () => {
21 | navigate("/genres");
22 | };
23 |
24 | // Handle clicking on individual genres
25 | const handleClick = (genre) => {
26 | const genreName = genre.name
27 | .toLowerCase()
28 | .replace(/\s+/g, "-")
29 | .replace(/\//g, "-");
30 | navigate(`/genres/${genreName}`);
31 | };
32 |
33 | useEffect(() => {
34 | const fetchGenres = async () => {
35 | try {
36 | const response = await fetch(
37 | "https://tunesmusicapp.onrender.com/genres"
38 | );
39 | const data = await response.json();
40 | // Assuming the API returns a `data` array with genre details
41 | setGenres(data.slice(0, 3)); // Fetch only the 2nd, 3rd, and 4th genres
42 | } catch (error) {
43 | console.error("Error fetching genres:", error);
44 | } finally {
45 | setIsLoading(false); // Set loading to false once data is fetched
46 | }
47 | };
48 |
49 | fetchGenres();
50 | }, []);
51 |
52 | return (
53 |
54 |
61 | Explore Genres
62 |
63 |
71 | Show all
72 |
73 |
74 | {isMobile ? (
75 | isLoading || genres.length === 0 ? (
76 |
77 |
78 |
79 | ) : (
80 |
81 | )
82 | ) : (
83 |
84 | {isLoading
85 | ? Array.from({ length: 3 }).map((_, index) => (
86 |
87 |
88 |
89 | ))
90 | : genres.map((genre) => (
91 | handleClick(genre)}
96 | />
97 | ))}
98 |
99 | )}
100 |
101 |
102 | );
103 | };
104 |
105 | export default FilteredGenres;
106 |
--------------------------------------------------------------------------------
/client/src/assets/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Container, Text, useColorModeValue } from "@chakra-ui/react";
3 |
4 | const Footer = () => {
5 | const gradientBg = useColorModeValue(
6 | "linear(to-r, gray.900, gray.700)",
7 | "linear(to-r, gray.900, gray.700)"
8 | );
9 |
10 | return (
11 |
19 |
20 | © 2024 Tunes. All rights reserved.
21 |
22 |
23 | );
24 | };
25 |
26 | export default Footer;
27 |
--------------------------------------------------------------------------------
/client/src/assets/components/GenreCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Image, Text } from "@chakra-ui/react";
3 |
4 | const GenreCard = ({ genre, coverImage, onClick }) => {
5 | return (
6 |
16 |
24 |
34 | {genre}
35 |
36 |
37 | );
38 | };
39 |
40 | export default GenreCard;
41 |
--------------------------------------------------------------------------------
/client/src/assets/components/LikedSongs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 |
3 | import { Container, Row, Col } from "react-bootstrap";
4 |
5 | import {
6 | Box,
7 | Flex,
8 | Heading,
9 | Image,
10 | Skeleton,
11 | Text,
12 | IconButton,
13 | Tooltip,
14 | } from "@chakra-ui/react";
15 | import { FaPlay, FaMinus } from "react-icons/fa";
16 |
17 | const LikedSongs = () => {
18 | const [likedSongs, setLikedSongs] = useState([]);
19 | const [isLoading, setIsLoading] = useState(true);
20 |
21 | useEffect(() => {
22 | const fetchLikedSongs = async () => {
23 | try {
24 | const response = await fetch(
25 | "https://tunesmusicapp.onrender.com/user",
26 | {
27 | credentials: "include",
28 | }
29 | );
30 | const data = await response.json();
31 | setLikedSongs(data.likedSongs || []);
32 | } catch (err) {
33 | console.error("Error fetching liked songs", err);
34 | } finally {
35 | setTimeout(() => {
36 | setIsLoading(false);
37 | }, 2000);
38 | }
39 | };
40 |
41 | fetchLikedSongs();
42 | }, []);
43 |
44 | const handleDelete = async (index) => {
45 | try {
46 | const songToDelete = likedSongs[index];
47 | const response = await fetch(
48 | "https://tunesmusicapp.onrender.com/deleteLikedSong",
49 | {
50 | method: "DELETE",
51 | headers: {
52 | "Content-Type": "application/json",
53 | },
54 | credentials: "include",
55 | body: JSON.stringify({
56 | songId: songToDelete._id,
57 | }),
58 | }
59 | );
60 | if (response.ok) {
61 | const updatedSongs = likedSongs.filter((_, i) => i !== index);
62 | setLikedSongs(updatedSongs);
63 | } else {
64 | console.error("Failed to delete song");
65 | }
66 | } catch (error) {
67 | console.error("Error deleting song:", error);
68 | }
69 | };
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
81 | Liked Songs
82 |
83 |
84 |
85 |
86 |
87 |
88 | {isLoading ? (
89 |
90 | ) : likedSongs.length > 0 ? (
91 | likedSongs.map((song, index) => (
92 |
100 |
113 |
114 |
121 |
122 | {song.name}
123 |
124 |
125 |
126 |
127 |
134 | }
141 | onClick={() => window.open(song.link, "_blank")}
142 | />
143 |
144 |
151 | }
155 | color="red.500"
156 | boxSize={6}
157 | p={1}
158 | onClick={() => handleDelete(index)}
159 | />
160 |
161 |
162 |
163 |
164 | ))
165 | ) : (
166 |
167 |
175 | No Liked Songs
176 |
177 |
178 | )}
179 |
180 |
181 | );
182 | };
183 |
184 | export default LikedSongs;
185 |
--------------------------------------------------------------------------------
/client/src/assets/components/Profile.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Row, Col, Image, Card, Button, Modal, Form } from "react-bootstrap";
3 | import {
4 | Box,
5 | Container,
6 | Flex,
7 | Spinner,
8 | Heading,
9 | Text,
10 | background,
11 | } from "@chakra-ui/react";
12 |
13 | import { useLocation, useParams } from "react-router-dom";
14 | import { Skeleton } from "@chakra-ui/react";
15 | import { EditIcon } from "@chakra-ui/icons";
16 |
17 | const Profile = () => {
18 | const [username, setUsername] = useState("");
19 | const [bio, setBio] = useState("");
20 | const [showModal, setShowModal] = useState(false);
21 | const [newBio, setNewBio] = useState("");
22 | const location = useLocation();
23 | const { email } = useParams();
24 | const isSearchPage = location.pathname === `/${email}/search`;
25 |
26 | //FETCHING USERDATA
27 | useEffect(() => {
28 | const fetchUserData = async () => {
29 | try {
30 | const response = await fetch(
31 | "https://tunesmusicapp.onrender.com/user",
32 | {
33 | credentials: "include",
34 | }
35 | );
36 | const data = await response.json();
37 | setUsername(data.name);
38 | setBio(data.bio);
39 | } catch (error) {
40 | console.error("Error fetching user data:", error);
41 | }
42 | };
43 |
44 | fetchUserData();
45 | }, []);
46 |
47 | //GETTING BIO
48 | const handleEditBio = () => {
49 | setNewBio(bio);
50 | setShowModal(true);
51 | };
52 |
53 | const handleSaveBio = async () => {
54 | try {
55 | const response = await fetch(
56 | "https://tunesmusicapp.onrender.com/updateBio",
57 | {
58 | method: "POST",
59 | headers: {
60 | "Content-Type": "application/json",
61 | },
62 | credentials: "include",
63 | body: JSON.stringify({ bio: newBio }),
64 | }
65 | );
66 | if (response.ok) {
67 | setBio(newBio);
68 | setShowModal(false);
69 | } else {
70 | console.error("Failed to update bio");
71 | }
72 | } catch (error) {
73 | console.error("Error updating bio:", error);
74 | }
75 | };
76 |
77 | //LOADING STATE
78 | const [isLoading, setIsLoading] = useState(true);
79 |
80 | useEffect(() => {
81 | const timer = setTimeout(() => {
82 | setIsLoading(false);
83 | }, 2000); // simulate loading time
84 | return () => clearTimeout(timer);
85 | }, []);
86 |
87 | return (
88 |
89 |
90 |
91 | {isLoading ? (
92 |
93 | ) : (
94 |
108 |
109 |
113 | {username}
114 |
115 |
116 |
122 |
123 | {bio}
124 |
125 |
126 | {!isSearchPage && (
127 |
137 |
138 | {bio ? "Edit bio" : "Add bio"}
139 |
140 |
144 |
145 |
146 |
147 | )}
148 |
149 |
150 |
151 | )}
152 |
153 |
154 |
155 | setShowModal(false)}>
156 |
157 | {bio ? "Edit bio" : "Add bio"}
158 |
159 |
160 |
162 | Bio
163 | setNewBio(e.target.value)}
168 | />
169 |
170 |
171 |
172 |
173 | setShowModal(false)}>
174 | Cancel
175 |
176 |
177 | Save
178 |
179 |
180 |
181 |
182 | );
183 | };
184 |
185 | export default Profile;
186 |
--------------------------------------------------------------------------------
/client/src/assets/components/SearchFavSongs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Row, Col, Card } from "react-bootstrap";
3 | import { useParams } from "react-router-dom";
4 | import { FaRegHeart, FaHeart } from "react-icons/fa";
5 | import { Skeleton } from "@chakra-ui/react";
6 | import { Box, Container, Flex, Image, Text } from "@chakra-ui/react";
7 | import { FaPlay } from "react-icons/fa";
8 | import { IconButton, Tooltip } from "@chakra-ui/react";
9 |
10 | const SearchFavSongs = () => {
11 | const [userData, setUserData] = useState(null);
12 | const [liked, setLiked] = useState([]);
13 | const { email } = useParams();
14 | const [isLoading, setIsLoading] = useState(true);
15 |
16 | const handleLike = async (index) => {
17 | const newLiked = [...liked];
18 | newLiked[index] = !newLiked[index];
19 | setLiked(newLiked);
20 |
21 | const song = userData.favouriteSongs[index];
22 |
23 | try {
24 | await fetch("https://tunesmusicapp.onrender.com/likedSong", {
25 | method: "POST",
26 | headers: {
27 | "Content-Type": "application/json",
28 | },
29 | credentials: "include",
30 | body: JSON.stringify({
31 | songName: song.name,
32 | songLink: song.link,
33 | }),
34 | });
35 | } catch (err) {
36 | console.error("Error toggling like for song", err);
37 | }
38 | };
39 |
40 | useEffect(() => {
41 | const fetchUserData = async () => {
42 | try {
43 | const response = await fetch(
44 | "https://tunesmusicapp.onrender.com/searchUser",
45 | {
46 | method: "POST",
47 | headers: {
48 | "Content-Type": "application/json",
49 | },
50 | credentials: "include",
51 | body: JSON.stringify({ query: email }),
52 | }
53 | );
54 | const data = await response.json();
55 | setUserData(data);
56 |
57 | if (data.favouriteSongs) {
58 | // Fetch the logged-in user's liked songs to set the initial liked state
59 | const userResponse = await fetch(
60 | "https://tunesmusicapp.onrender.com/user",
61 | {
62 | method: "GET",
63 | headers: {
64 | "Content-Type": "application/json",
65 | },
66 | credentials: "include",
67 | }
68 | );
69 | const userData = await userResponse.json();
70 | const likedSongs = userData.likedSongs || [];
71 |
72 | const newLiked = data.favouriteSongs.map((song) =>
73 | likedSongs.some(
74 | (likedSong) =>
75 | likedSong.name === song.name && likedSong.link === song.link
76 | )
77 | );
78 | setLiked(newLiked);
79 | }
80 | } catch (err) {
81 | console.error("Error fetching user data", err);
82 | } finally {
83 | setTimeout(() => {
84 | setIsLoading(false);
85 | }, 2000);
86 | }
87 | };
88 |
89 | fetchUserData();
90 | }, [email]);
91 |
92 | if (!userData || userData.favouriteSongs.length === 0) {
93 | return (
94 |
95 |
96 |
97 |
98 |
99 |
104 | Favourite Songs
105 |
106 |
107 |
115 | No Favourite Songs
116 |
117 |
118 |
119 |
120 |
121 | );
122 | }
123 |
124 | return (
125 |
126 |
127 |
128 |
129 |
130 |
131 |
136 | Favourite Songs
137 |
138 |
139 |
140 | {userData.favouriteSongs.map((song, index) => (
141 |
142 | {isLoading ? (
143 |
144 | ) : (
145 |
158 |
159 |
166 |
171 | {song.name}
172 |
173 |
174 |
175 |
176 |
183 | }
193 | onClick={() => window.open(song.link, "_blank")}
194 | />
195 |
196 |
197 |
204 | handleLike(index)}
211 | >
212 | {liked[index] ? (
213 |
214 | ) : (
215 |
216 | )}
217 |
218 |
219 |
220 |
221 | )}
222 |
223 | ))}
224 |
225 |
226 |
227 |
228 |
229 | );
230 | };
231 |
232 | export default SearchFavSongs;
233 |
--------------------------------------------------------------------------------
/client/src/assets/components/SearchLikedSongs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Row, Col, Card } from "react-bootstrap";
3 | import {
4 | Container,
5 | Image,
6 | Box,
7 | Text,
8 | Flex,
9 | Tooltip,
10 | IconButton,
11 | } from "@chakra-ui/react";
12 | import { FaPlay } from "react-icons/fa";
13 | import { useParams } from "react-router-dom";
14 | import { FaRegHeart, FaHeart } from "react-icons/fa";
15 | import { Skeleton } from "@chakra-ui/react";
16 |
17 | const SearchLikedSongs = () => {
18 | const [userData, setUserData] = useState(null);
19 | const [liked, setLiked] = useState([]);
20 | const { email } = useParams();
21 | const [isLoading, setIsLoading] = useState(true);
22 |
23 | const handleLike = async (index) => {
24 | const song = userData.likedSongs[index];
25 | const isLiked = liked[index];
26 |
27 | try {
28 | if (isLiked) {
29 | // Unlike the song
30 | await fetch("https://tunesmusicapp.onrender.com/deleteLikedSong", {
31 | method: "DELETE",
32 | headers: {
33 | "Content-Type": "application/json",
34 | },
35 | credentials: "include",
36 | body: JSON.stringify({
37 | songId: song._id,
38 | }),
39 | });
40 | } else {
41 | // Like the song
42 | await fetch("https://tunesmusicapp.onrender.com/likedSong", {
43 | method: "POST",
44 | headers: {
45 | "Content-Type": "application/json",
46 | },
47 | credentials: "include",
48 | body: JSON.stringify({
49 | songName: song.name,
50 | songLink: song.link,
51 | }),
52 | });
53 | }
54 |
55 | // Update local state after the API call
56 | const newLiked = [...liked];
57 | newLiked[index] = !newLiked[index];
58 | setLiked(newLiked);
59 | } catch (err) {
60 | console.error("Error toggling like for song", err);
61 | }
62 | };
63 |
64 | useEffect(() => {
65 | const fetchUserData = async () => {
66 | try {
67 | const response = await fetch(
68 | "https://tunesmusicapp.onrender.com/searchUser",
69 | {
70 | method: "POST",
71 | headers: {
72 | "Content-Type": "application/json",
73 | },
74 | credentials: "include",
75 | body: JSON.stringify({ query: email }),
76 | }
77 | );
78 | const data = await response.json();
79 | console.log("Fetched user data:", data); // Debug log
80 |
81 | setUserData(data);
82 |
83 | if (data.likedSongs) {
84 | const userResponse = await fetch(
85 | "https://tunesmusicapp.onrender.com/user",
86 | {
87 | method: "GET",
88 | headers: {
89 | "Content-Type": "application/json",
90 | },
91 | credentials: "include",
92 | }
93 | );
94 | const userData = await userResponse.json();
95 | console.log("Logged-in user data:", userData); // Debug log
96 |
97 | const likedSongs = userData.likedSongs || [];
98 | const newLiked = data.likedSongs.map((song) =>
99 | likedSongs.some(
100 | (likedSong) =>
101 | likedSong.name === song.name && likedSong.link === song.link
102 | )
103 | );
104 | console.log("New liked state:", newLiked); // Debug log
105 | setLiked(newLiked);
106 | }
107 | } catch (err) {
108 | console.error("Error fetching user data", err);
109 | } finally {
110 | setIsLoading(false);
111 | }
112 | };
113 |
114 | fetchUserData();
115 | }, [email]);
116 |
117 | if (isLoading) {
118 | return (
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 | }
130 |
131 | if (!userData || !userData.likedSongs || userData.likedSongs.length === 0) {
132 | return (
133 |
134 |
135 |
136 |
137 |
138 |
143 | Liked Songs
144 |
145 |
146 |
154 | No Liked Songs
155 |
156 |
157 |
158 |
159 |
160 | );
161 | }
162 |
163 | return (
164 |
165 |
166 |
167 |
168 |
169 |
170 |
175 | Liked Songs
176 |
177 |
178 |
179 | {userData.likedSongs.map((song, index) => (
180 |
184 | {isLoading ? (
185 |
186 | ) : (
187 |
200 |
201 |
208 |
213 | {song.name}
214 |
215 |
216 |
217 |
218 |
225 | }
235 | onClick={() => window.open(song.link, "_blank")}
236 | />
237 |
238 |
239 |
246 | handleLike(index)}
253 | >
254 | {liked[index] ? (
255 |
256 | ) : (
257 |
258 | )}
259 |
260 |
261 |
262 |
263 | )}
264 |
265 | ))}
266 |
267 |
268 |
269 |
270 |
271 | );
272 | };
273 |
274 | export default SearchLikedSongs;
275 |
--------------------------------------------------------------------------------
/client/src/assets/components/SearchProfile.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useParams } from "react-router-dom";
3 | import AppNavbar from "./AppNavbar";
4 | import { Container, Box, Flex, Text, Skeleton } from "@chakra-ui/react";
5 | import { Row, Col } from "react-bootstrap";
6 | import NotFound from "../../LandingPage";
7 |
8 | const SearchProfile = () => {
9 | const [userData, setUserData] = useState(null);
10 | const [isLoading, setIsLoading] = useState(true);
11 | const { email } = useParams();
12 |
13 | useEffect(() => {
14 | const fetchUserData = async () => {
15 | try {
16 | const response = await fetch(
17 | `https://tunesmusicapp.onrender.com/searchUser`,
18 | {
19 | method: "POST",
20 | headers: {
21 | "Content-Type": "application/json",
22 | },
23 | credentials: "include",
24 | body: JSON.stringify({ query: email }),
25 | }
26 | );
27 | if (!response.ok) {
28 | throw new Error("Network response was not ok");
29 | }
30 | const data = await response.json();
31 | setUserData(data);
32 | } catch (err) {
33 | console.error("Error fetching user data", err);
34 | } finally {
35 | setIsLoading(false); // Set loading to false after the data is fetched
36 | }
37 | };
38 |
39 | const timer = setTimeout(() => {
40 | fetchUserData();
41 | setIsLoading(false);
42 | }, 2000); // Simulate loading time of 2 seconds
43 |
44 | return () => clearTimeout(timer); // Cleanup the timer on component unmount
45 | }, [email]);
46 |
47 | if (isLoading) {
48 | return (
49 | <>
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | >
59 | );
60 | }
61 |
62 | if (!userData) {
63 | return ;
64 | }
65 |
66 | return (
67 | <>
68 |
69 |
70 |
71 |
72 |
86 |
90 |
91 |
92 |
96 | {userData.name}
97 |
98 |
99 |
100 |
101 | {userData.bio}
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | >
111 | );
112 | };
113 |
114 | export default SearchProfile;
115 |
--------------------------------------------------------------------------------
/client/src/assets/components/SongCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box, Image, Text, IconButton, Flex } from "@chakra-ui/react";
3 | import { FaPlay, FaRegHeart, FaHeart } from "react-icons/fa";
4 |
5 | const SongCard = ({ song, liked, onToggleLike }) => {
6 | return (
7 |
19 | {/* Left: Album cover or icon */}
20 |
21 |
27 |
28 |
29 | {song.title}
30 |
31 |
32 | {song.artist} {/* Artist Name */}
33 |
34 |
35 |
36 |
37 | {/* Right: Play button and like button */}
38 |
39 | }
49 | onClick={() => window.open(song.link, "_blank")}
50 | />
51 |
52 | : }
55 | fontSize="20px"
56 | bg="transparent"
57 | color={liked ? "#1DB954" : "white"} // Green if liked
58 | _hover={{ color: liked ? "green.400" : "red.400" }}
59 | onClick={onToggleLike}
60 | />
61 |
62 |
63 | );
64 | };
65 |
66 | export default SongCard;
67 |
--------------------------------------------------------------------------------
/client/src/assets/components/UserNotFound.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Spinner, Box, Text } from "@chakra-ui/react";
3 | import AppNavbar from "./AppNavbar";
4 |
5 | const UserNotFound = () => {
6 | const [isLoading, setIsLoading] = useState(true);
7 |
8 | useEffect(() => {
9 | // Simulate a loading delay
10 | const timer = setTimeout(() => {
11 | setIsLoading(false);
12 | }, 2000); // Adjust the timeout as needed
13 |
14 | return () => clearTimeout(timer);
15 | }, []);
16 |
17 | if (isLoading) {
18 | return (
19 |
25 |
26 |
27 | );
28 | }
29 |
30 | return (
31 |
32 |
33 |
34 |
35 | User Not Found
36 |
37 | The user you are looking for does not exist.
38 |
39 |
40 | );
41 | };
42 |
43 | export default UserNotFound;
44 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amithkumar10/TunesMusicApp/976346d4eca4b0d970f9aa1b8ad60150cfc2c9c6/client/src/index.css
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { ChakraProvider } from "@chakra-ui/react";
4 | import App from "./App.jsx";
5 | import "./index.css";
6 | import theme from "./Theme.jsx";
7 |
8 | ReactDOM.createRoot(document.getElementById("root")).render(
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/client/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/(.*)",
5 | "destination": "/"
6 | }
7 | ],
8 | "headers": [
9 | {
10 | "source": "/sitemap.xml",
11 | "headers": [
12 | {
13 | "key": "Content-Type",
14 | "value": "application/xml"
15 | }
16 | ]
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Login-Register",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "dependencies": {
8 | "dotenv": "^16.4.5"
9 | }
10 | },
11 | "node_modules/dotenv": {
12 | "version": "16.4.5",
13 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
14 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
15 | "engines": {
16 | "node": ">=12"
17 | },
18 | "funding": {
19 | "url": "https://dotenvx.com"
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "dotenv": "^16.4.5"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import mongoose from "mongoose";
3 | import cors from "cors";
4 | import EmployeeModel from "./models/Employee.js";
5 | import ArtistModel from "./models/artist.js";
6 | import SongModel from "./models/song.js";
7 | import GenreModel from "./models/genre.js";
8 | import bcrypt from "bcrypt";
9 | import jwt from "jsonwebtoken";
10 | import cookieParser from "cookie-parser";
11 | import axios from "axios";
12 |
13 | // CONNECTING TO DATABASE
14 | mongoose
15 | .connect(
16 | "mongodb+srv://amith:Hello%40mith18@mongodb.tyrxhwb.mongodb.net/Employee"
17 | )
18 | .then(() => {
19 | console.log("Connected to MongoDB database:", mongoose.connection.name);
20 | })
21 | .catch((error) => {
22 | console.error("Error connecting to MongoDB:", error.message);
23 | });
24 |
25 | // SETTING UP EXPRESS
26 | const app = express();
27 | app.use(express.json());
28 | app.use(
29 | //Cross-Origin Resource Sharing (CORS)
30 | cors({
31 | // origin: ["https://tunes-music-app.vercel.app"],
32 | origin: ["http://localhost:5173", "https://tunes-music-app.vercel.app"],
33 | credentials: true,
34 | methods: ["GET", "POST", "DELETE", "PUT"],
35 | })
36 | );
37 | app.use(cookieParser());
38 |
39 | // SECRET FOR JWT
40 |
41 | const secret = "secret";
42 |
43 | const keepAlive = () => {
44 | const appUrl = "https://tunesmusicapp.onrender.com"; // Replace with your actual URL
45 |
46 | axios
47 | .get(appUrl)
48 | .then((response) => {
49 | console.log(`Pinged ${appUrl} successfully.`);
50 | })
51 | .catch((error) => {
52 | console.log(`Error pinging the server: ${error.message}`);
53 | });
54 | };
55 |
56 | // Start the keep-alive function on server start
57 | const startKeepAlive = () => {
58 | keepAlive(); // Send an initial ping
59 |
60 | // Set the function to run every 14 minutes (840 seconds)
61 | setInterval(keepAlive, 840 * 1000); // 840 seconds = 14 minutes
62 | };
63 |
64 | // Call the startKeepAlive function once the server is running
65 | startKeepAlive();
66 |
67 | // REGISTER ROUTE
68 | app.post("/register", (req, res) => {
69 | const { name, email, password } = req.body;
70 | bcrypt
71 | .hash(password, 10)
72 | .then((hash) => {
73 | EmployeeModel.create({ name, email, password: hash })
74 | .then((employees) => res.json(employees))
75 | .catch((err) => {
76 | if (err.code === 11000 && err.keyPattern.email) {
77 | res.status(400).json("Duplicate Email");
78 | } else {
79 | res.status(500).json(err);
80 | }
81 | });
82 | })
83 | .catch((err) => {
84 | console.error("Error hashing password:", err.message);
85 | res.status(500).json("Error hashing password");
86 | });
87 | });
88 |
89 | // LOGIN ROUTE
90 | app.post("/login", (req, res) => {
91 | const { email, password } = req.body;
92 | EmployeeModel.findOne({ email: email })
93 | .then((user) => {
94 | if (user) {
95 | bcrypt.compare(password, user.password, (err, response) => {
96 | if (err) {
97 | console.error("Error comparing passwords:", err);
98 | res.status(500).json("Error comparing passwords");
99 | } else if (response) {
100 | const token = jwt.sign(
101 | { email: user.email, id: user._id },
102 | secret,
103 | {
104 | expiresIn: "1d",
105 | }
106 | );
107 | // Set the cookie with SameSite=None and Secure=true
108 | res.cookie("IAMIN", token, {
109 | httpOnly: true,
110 | secure: true, // Set to true for cross-origin requests
111 | sameSite: "None", // Required for cross-origin cookies
112 | });
113 | res.json({ message: "Success", email: user.email });
114 | } else {
115 | res.status(401).json("Incorrect password"); // Return status 401 for incorrect password
116 | }
117 | });
118 | } else {
119 | res.status(404).json("User not found"); // Return status 404 for user not found
120 | }
121 | })
122 | .catch((err) => {
123 | console.error("Error finding user:", err);
124 | res.status(500).json("Error finding user");
125 | });
126 | });
127 |
128 | // VERIFY TOKEN MIDDLEWARE
129 | const verifyToken = (req, res, next) => {
130 | const token = req.cookies.IAMIN;
131 | if (!token) {
132 | return res.status(403).json("Not authenticated");
133 | }
134 |
135 | jwt.verify(token, secret, (err, decoded) => {
136 | if (err) {
137 | return res.status(403).json("Token expired");
138 | }
139 | req.user = decoded;
140 | console.log("Decoded token:", decoded);
141 | next();
142 | });
143 | };
144 |
145 | // PROTECTED ROUTE
146 | app.get("/", verifyToken, (req, res) => {
147 | res.json("Success");
148 | });
149 |
150 | // LOGOUT ROUTE
151 | app.get("/logout", (req, res) => {
152 | res.cookie("IAMIN", "", {
153 | expires: new Date(0),
154 | httpOnly: true,
155 | secure: false,
156 | });
157 | res.json("Logged out");
158 | });
159 |
160 | //GET ALL USERS & THEIR DATA
161 | app.get("/users", (req, res) => {
162 | EmployeeModel.find({})
163 | .then((users) => {
164 | if (!users || users.length === 0) {
165 | return res.status(404).json("No user found");
166 | } else {
167 | res.json(users);
168 | }
169 | })
170 | .catch((err) => res.status(500).json("Error fetching user data"));
171 | });
172 |
173 | //GET USER
174 | app.get("/user", verifyToken, (req, res) => {
175 | EmployeeModel.findById(
176 | req.user.id,
177 | " email name bio favouriteSongs likedSongs"
178 | ) // Select name and bio fields
179 | .then((user) => {
180 | if (!user) {
181 | return res.status(404).json("User not found");
182 | } else {
183 | res.json({
184 | email: user.email,
185 | name: user.name,
186 | bio: user.bio,
187 | favouriteSongs: user.favouriteSongs,
188 | likedSongs: user.likedSongs,
189 | });
190 | }
191 | })
192 | .catch((err) => res.status(500).json("Error fetching user data"));
193 | });
194 |
195 | // SEARCH USER ROUTE
196 |
197 | app.post("/searchUser", verifyToken, async (req, res) => {
198 | const { query } = req.body;
199 |
200 | try {
201 | // Find a user by either email or name
202 | const user = await EmployeeModel.findOne(
203 | {
204 | $or: [
205 | { email: query },
206 | { name: { $regex: query, $options: "i" } }, // Case-insensitive search for name
207 | ],
208 | },
209 | "name bio favouriteSongs likedSongs"
210 | );
211 |
212 | if (!user) {
213 | console.log("User not found");
214 | return res.status(404).json({ message: "User not found" });
215 | }
216 |
217 | console.log("User found:", user);
218 | res.json(user);
219 | } catch (err) {
220 | console.error("Error searching for user:", err);
221 | res.status(500).json({ message: "Error searching for user" });
222 | }
223 | });
224 |
225 | //SUGGEST USERS
226 | app.post("/suggestUsers", verifyToken, async (req, res) => {
227 | const { query } = req.body;
228 |
229 | try {
230 | // Find users whose names match the query (case-insensitive)
231 | const users = await EmployeeModel.find(
232 | {
233 | name: { $regex: query, $options: "i" }, // Case-insensitive search for name
234 | },
235 | "name bio favouriteSongs likedSongs" // Specify fields to return
236 | ).limit(5); // Limit results to 5 for performance
237 |
238 | if (users.length === 0) {
239 | console.log("No users found");
240 | return res.status(404).json({ message: "No users found" });
241 | }
242 |
243 | console.log("Users found:", users);
244 | res.json(users);
245 | } catch (err) {
246 | console.error("Error suggesting users:", err);
247 | res.status(500).json({ message: "Error suggesting users" });
248 | }
249 | });
250 |
251 | // UPDATE BIO ROUTE
252 | app.post("/updateBio", verifyToken, (req, res) => {
253 | const { bio } = req.body;
254 | EmployeeModel.findByIdAndUpdate(req.user.id, { bio: bio }, { new: true })
255 | .then((user) => res.json(user))
256 | .catch((err) => {
257 | console.error("Error updating bio:", err); // Debugging statement
258 | res.status(500).json("Error updating bio");
259 | });
260 | });
261 |
262 | // ADD FAVOURITE SONGS ROUTE
263 | app.post("/addFavouriteSong", verifyToken, async (req, res) => {
264 | const { songName, songLink } = req.body;
265 |
266 | //Validation;
267 | if (!songName || !songLink) {
268 | console.log("Add both");
269 | return res.status(400).json("Song name and link are required.");
270 | }
271 |
272 | try {
273 | const user = await EmployeeModel.findByIdAndUpdate(
274 | req.user.id,
275 | { $push: { favouriteSongs: { name: songName, link: songLink } } },
276 | { new: true }
277 | );
278 |
279 | if (!user) {
280 | return res.status(404).json("User not found.");
281 | }
282 |
283 | res.json(user);
284 | } catch (err) {
285 | console.error("Error adding favorite song:", err);
286 | res.status(500).json("Error adding favorite song");
287 | }
288 | });
289 |
290 | // DELETE SONG
291 | app.delete("/deleteFavouriteSong", verifyToken, (req, res) => {
292 | const { songId } = req.body;
293 | const userId = req.user.id;
294 |
295 | EmployeeModel.findByIdAndUpdate(
296 | userId,
297 | { $pull: { favouriteSongs: { _id: songId } } },
298 | { new: true }
299 | )
300 | .then((user) => res.json(user))
301 | .catch((err) => res.status(500).json(err));
302 | });
303 |
304 | // LIKED SONGS ROUTE (like/unlike a song)
305 | app.post("/likedSong", verifyToken, async (req, res) => {
306 | const { songName, songLink } = req.body;
307 | try {
308 | const user = await EmployeeModel.findById(req.user.id);
309 | const songIndex = user.likedSongs.findIndex(
310 | (song) => song.name === songName && song.link === songLink
311 | );
312 |
313 | if (songIndex > -1) {
314 | // Song already liked, so unlike it
315 | user.likedSongs.splice(songIndex, 1);
316 | } else {
317 | // Song not liked, so like it
318 | user.likedSongs.push({ name: songName, link: songLink });
319 | }
320 |
321 | await user.save();
322 | res.json(user.likedSongs); // Return updated liked songs
323 | } catch (err) {
324 | console.error("Error toggling liked song:", err);
325 | res.status(500).json("Error toggling liked song");
326 | }
327 | });
328 |
329 | //TO DELETE LIKED SONG
330 | app.delete("/deleteLikedSong", verifyToken, (req, res) => {
331 | const { songId } = req.body;
332 | EmployeeModel.findByIdAndUpdate(
333 | req.user.id,
334 | { $pull: { likedSongs: { _id: songId } } },
335 | { new: true }
336 | )
337 | .then((user) => res.json(user))
338 | .catch((err) => {
339 | console.error("Error deleting liked song:", err);
340 | res.status(500).json("Error deleting liked song");
341 | });
342 | });
343 |
344 | // EXPLORE PAGE
345 | // Get all artists
346 | app.get("/artists", async (req, res) => {
347 | try {
348 | const artists = await ArtistModel.find(); // Fetch all artists
349 | res.status(200).json(artists); // Send the data as response
350 | } catch (error) {
351 | res.status(500).json({ message: "Error fetching artists", error });
352 | }
353 | });
354 |
355 | // Get all genres
356 | app.get("/genres", async (req, res) => {
357 | try {
358 | const genres = await GenreModel.find(); // Fetch all genres
359 | res.status(200).json(genres); // Send the data as response
360 | } catch (error) {
361 | res.status(500).json({ message: "Error fetching genres", error });
362 | }
363 | });
364 |
365 | // Get all songs
366 | app.get("/songs", async (req, res) => {
367 | try {
368 | const songs = await SongModel.find(); // Fetch all songs
369 | res.status(200).json(songs); // Send the data as response
370 | } catch (error) {
371 | res.status(500).json({ message: "Error fetching songs", error });
372 | }
373 | });
374 |
375 | // Get songs by artist name
376 | app.get("/artists/:artistName", async (req, res) => {
377 | try {
378 | const artistName = req.params.artistName;
379 | console.log("Looking for artist:", artistName); // Debug log
380 |
381 | const artist = await ArtistModel.findOne({ name: artistName });
382 |
383 | if (!artist) {
384 | return res.status(404).json({ message: "Artist not found" });
385 | }
386 |
387 | res.status(200).json(artist);
388 | } catch (err) {
389 | console.error("Error:", err);
390 | res.status(500).json({ message: "Server error" });
391 | }
392 | });
393 |
394 | // Get songs by genre name
395 | app.get("/genre/:genreName", async (req, res) => {
396 | try {
397 | const genreName = req.params.genreName;
398 | console.log("Looking for genre:", genreName); // Debug log
399 |
400 | const genre = await GenreModel.findOne({ name: genreName });
401 |
402 | if (!genre) {
403 | return res.status(404).json({ message: "Genre not found" });
404 | }
405 |
406 | res.status(200).json(genre); // Send the songs associated with this genre
407 | } catch (err) {
408 | console.error("Error:", err);
409 | res.status(500).json({ message: "Server error" });
410 | }
411 | });
412 |
413 | app.listen(3000, () => {
414 | console.log("Server is working");
415 | });
416 |
--------------------------------------------------------------------------------
/server/models/Employee.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const EmployeeSchema = new mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: true,
7 | },
8 | email: {
9 | type: String,
10 | required: true,
11 | unique: true,
12 | },
13 | bio: {
14 | type: String,
15 | default: "",
16 | },
17 | favouriteSongs: {
18 | type: [
19 | {
20 | name: { type: String, required: true },
21 | link: { type: String, required: true },
22 | },
23 | ],
24 | default: [],
25 | },
26 | likedSongs: {
27 | type: [
28 | {
29 | name: { type: String, required: true },
30 | link: { type: String, required: true },
31 | },
32 | ],
33 | default: [],
34 | },
35 | password: {
36 | type: String,
37 | required: true,
38 | },
39 | });
40 |
41 | const EmployeeModel = mongoose.model("employees", EmployeeSchema);
42 |
43 | export default EmployeeModel;
44 |
--------------------------------------------------------------------------------
/server/models/artist.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const ArtistSchema = new mongoose.Schema(
4 | {
5 | name: {
6 | type: String,
7 | required: true,
8 | },
9 | bio: {
10 | type: String,
11 | default: "",
12 | },
13 | image: {
14 | type: String,
15 | required: true,
16 | },
17 | },
18 | { timestamps: true }
19 | );
20 |
21 | const ArtistModel = mongoose.model("Artist", ArtistSchema);
22 |
23 | export default ArtistModel;
24 |
--------------------------------------------------------------------------------
/server/models/genre.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const GenreSchema = new mongoose.Schema(
4 | {
5 | name: {
6 | type: String,
7 | required: true,
8 | },
9 | coverImage: {
10 | type: String,
11 | required: true,
12 | },
13 | },
14 | { timestamps: true }
15 | );
16 |
17 | const GenreModel = mongoose.model("Genre", GenreSchema);
18 |
19 | export default GenreModel;
20 |
--------------------------------------------------------------------------------
/server/models/song.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const SongSchema = new mongoose.Schema(
4 | {
5 | title: {
6 | type: String,
7 | required: true,
8 | },
9 | artistId: {
10 | type: mongoose.Schema.Types.ObjectId,
11 | ref: "Artist", // References Artist model
12 | required: true,
13 | },
14 | genreId: {
15 | type: mongoose.Schema.Types.ObjectId,
16 | ref: "Genre", // References Genre model
17 | required: true,
18 | },
19 | albumCover: {
20 | type: String,
21 | required: true,
22 | },
23 | previewLink: {
24 | type: String,
25 | required: true,
26 | },
27 | artistName: {
28 | type: String,
29 | required: true,
30 | },
31 | },
32 | { timestamps: true }
33 | );
34 |
35 | const SongModel = mongoose.model("Song", SongSchema);
36 |
37 | export default SongModel;
38 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "type": "module",
6 | "main": "index.js",
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": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "axios": "^1.7.9",
17 | "bcrypt": "^5.1.1",
18 | "cookie-parser": "^1.4.6",
19 | "cors": "^2.8.5",
20 | "dotenv": "^16.4.5",
21 | "express": "^4.21.0",
22 | "jsonwebtoken": "^9.0.2",
23 | "mongoose": "^8.5.1",
24 | "nodemon": "^3.1.4"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------