diff --git a/qtify/package-lock.json b/qtify/package-lock.json index 84c0f2767a..c9620aeda4 100644 --- a/qtify/package-lock.json +++ b/qtify/package-lock.json @@ -11,6 +11,7 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", "@mui/base": "^5.0.0-beta.40", + "@mui/icons-material": "^5.16.6", "@mui/lab": "^5.0.0-alpha.173", "@mui/material": "^5.16.6", "@testing-library/jest-dom": "^5.17.0", @@ -3677,6 +3678,32 @@ "url": "https://opencollective.com/mui-org" } }, + "node_modules/@mui/icons-material": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.6.tgz", + "integrity": "sha512-ceNGjoXheH9wbIFa1JHmSc9QVjJUvh18KvHrR4/FkJCSi9HXJ+9ee1kUhCOEFfuxNF8UB6WWVrIUOUgRd70t0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/lab": { "version": "5.0.0-alpha.173", "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.173.tgz", diff --git a/qtify/package.json b/qtify/package.json index c4510d778b..dcc24c7bbc 100644 --- a/qtify/package.json +++ b/qtify/package.json @@ -6,6 +6,7 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", "@mui/base": "^5.0.0-beta.40", + "@mui/icons-material": "^5.16.6", "@mui/lab": "^5.0.0-alpha.173", "@mui/material": "^5.16.6", "@testing-library/jest-dom": "^5.17.0", diff --git a/qtify/src/App.css b/qtify/src/App.css index 74b5e05345..e247a5ffe9 100644 --- a/qtify/src/App.css +++ b/qtify/src/App.css @@ -36,3 +36,4 @@ transform: rotate(360deg); } } + diff --git a/qtify/src/App.js b/qtify/src/App.js index 579b5ef93a..328c1437b5 100644 --- a/qtify/src/App.js +++ b/qtify/src/App.js @@ -3,9 +3,25 @@ import Navbar from './components/Navbar/Navbar'; import Hero from './components/Hero/Hero'; import axios from 'axios'; -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import Section from './components/Section/Section'; import { Stack, Divider } from '@mui/material'; +import { MusicContext } from './MusicContext'; +import MusicBar from './components/MusicBar/MusicBar'; + +import { Routes, Route } from 'react-router-dom'; +import AlbumDetails from './components/AlbumDetails/AlbumDetails'; + + +export const fetchData = async (type) => { + try { + let res = await axios.get('https://qtify-backend-labs.crio.do/' + type) + + return res.data + } catch (e) { + console.log(e) + } +} function App() { @@ -13,16 +29,13 @@ function App() { const [topData, setTopData] = useState([]) const [songData, setSongData] = useState([]) const [genreData, setGenreData] = useState([]) + const [faqData, setFaqData] = useState([]) + + + const [selectedSong, setSelectedSong] = useState(null); + - const fetchData = async (type) => { - try { - let res = await axios.get('https://qtify-backend-labs.crio.do/' + type) - return res.data - } catch (e) { - console.log(e) - } - } useEffect(() => { const getData = async () => { @@ -34,6 +47,8 @@ function App() { await setSongData(data) data = await fetchData('genres') await setGenreData(data.data) + data = await fetchData('faq') + await setFaqData(data.data) } @@ -42,17 +57,29 @@ function App() { }, []) + + return ( -
- - - }> -
-
-
- - -
+ +
+ + + + + }> +
+
+
+
+ + + + } /> + } /> + +
+
); } diff --git a/qtify/src/MusicContext.js b/qtify/src/MusicContext.js new file mode 100644 index 0000000000..aa0f9e88dd --- /dev/null +++ b/qtify/src/MusicContext.js @@ -0,0 +1,4 @@ +import React from 'react'; + + +export const MusicContext = React.createContext() diff --git a/qtify/src/assets/music.mp3 b/qtify/src/assets/music.mp3 new file mode 100644 index 0000000000..edbe9e056b Binary files /dev/null and b/qtify/src/assets/music.mp3 differ diff --git a/qtify/src/components/AlbumDetails/AlbumDetails.js b/qtify/src/components/AlbumDetails/AlbumDetails.js new file mode 100644 index 0000000000..5d33fd8ca2 --- /dev/null +++ b/qtify/src/components/AlbumDetails/AlbumDetails.js @@ -0,0 +1,127 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import MusicBar from '../MusicBar/MusicBar'; +import axios from 'axios'; +import ArrowCircleLeftOutlinedIcon from '@mui/icons-material/ArrowCircleLeftOutlined'; +import { useNavigate } from 'react-router-dom'; +import { Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'; +import Pagination from '@mui/material/Pagination'; + +import { MusicContext } from "../../MusicContext"; + +function AlbumDetails() { + + const { id } = useParams(); + const [album, setAlbum] = useState(null); + const [page, setPage] = useState(1); + + const navigate = useNavigate(); + const { selectedSong, setSelectedSong } = React.useContext(MusicContext) + + const fetchAlbumData = async () => { + try { + const newData = await axios.get('https://qtify-backend-labs.crio.do/albums/new'); + const topData = await axios.get('https://qtify-backend-labs.crio.do/albums/top'); + const allData = [...newData.data, ...topData.data]; + const foundAlbum = allData.find(item => item.id === id); + setAlbum(foundAlbum); + } catch (error) { + console.error("Error fetching album data:", error); + } + }; + + useEffect(() => { + fetchAlbumData(); + }, [id]); + + const handleChange = (event, value) => { + setPage(value); + }; + + const songsPerPage = 13; + const startIndex = (page - 1) * songsPerPage; + const endIndex = startIndex + songsPerPage; + const currentSongs = album ? album.songs.slice(startIndex, endIndex) : []; + + if (!album) { + return
Loading...
; + } + + function totalDuration() { + let durationInMillis = album.songs.reduce((acc, curr) => curr.durationInMs + acc, 0); + const minutes = Math.floor(durationInMillis / 60000); // 1 minute = 60,000 milliseconds + return `${minutes} min`; + } + + return ( +
+ + + navigate('/')} style={{ cursor: 'pointer', height: '4%', width: '4%' }} /> + +
+ {album.title} +
+

{album.title}

+
+

{album.description}

+

{album.songs.length} songs • {totalDuration()} • {album.follows} Follows

+
+
+
+ +
+ +
+ + + + + + Title + Artist + Duration + + + + {currentSongs.map((song, index) => ( + setSelectedSong(song)} style={{ cursor: 'pointer' }}> + + {song.title} + {song.title} + + {song.artists.join(', ')} + {Math.ceil(song.durationInMs / 60000)} min + + ))} + +
+
+ + +
+ +
+ ); +} + +export default AlbumDetails; diff --git a/qtify/src/components/AlbumDetails/AlbumDetails.module.css b/qtify/src/components/AlbumDetails/AlbumDetails.module.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/qtify/src/components/Button/Button.jsx b/qtify/src/components/Button/Button.jsx index 0b295cbe82..d948dbcf68 100644 --- a/qtify/src/components/Button/Button.jsx +++ b/qtify/src/components/Button/Button.jsx @@ -1,9 +1,9 @@ import React from "react"; import styles from "./Button.module.css"; -export default function Button({ children }) { +export default function CustomButton({ children, onClick }) { return ( - ) diff --git a/qtify/src/components/Button/Button.module.css b/qtify/src/components/Button/Button.module.css index e7ac376cc2..0c557a0c95 100644 --- a/qtify/src/components/Button/Button.module.css +++ b/qtify/src/components/Button/Button.module.css @@ -3,7 +3,7 @@ color: var(--color-primary); border-radius: 8px; font-weight: bold; - padding: 14px 20px; + padding: 10px 20px; border: none; font-size: var(--font-size-base); font-family: var(--font-family); diff --git a/qtify/src/components/Carousel/Carousel.js b/qtify/src/components/Carousel/Carousel.js index fb667bdae7..9c9b808f26 100644 --- a/qtify/src/components/Carousel/Carousel.js +++ b/qtify/src/components/Carousel/Carousel.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo, useContext } from 'react'; import { Swiper, SwiperSlide } from 'swiper/react'; import 'swiper/css'; import 'swiper/css/navigation'; @@ -13,36 +13,60 @@ import TabContext from '@mui/lab/TabContext'; import TabList from '@mui/lab/TabList'; import TabPanel from '@mui/lab/TabPanel'; +import { MusicContext } from "../../MusicContext"; +import { useNavigate } from 'react-router-dom'; + export default function Carousel({ data, songs = false, genres }) { const [value, setValue] = useState('all'); + const { selectedSong, setSelectedSong } = useContext(MusicContext); + const navigate = useNavigate(); + // Handle tab change const handleChange = (event, newValue) => { setValue(newValue); }; - const renderSwiper = (filteredData) => ( - - {filteredData.map((group, index) => ( - - - {group.title} - - ))} - - ); + // Handle song selection + const handleSelectSong = (group) => { + setSelectedSong(group); + }; + + // Handle navigation to album details + const handleNavigate = (id) => { + navigate(`/albumdetails/${id}`); + }; + + // Memoize the Swiper component + const memoizedSwiper = useMemo(() => { + const renderSwiper = (filteredData) => ( + + {filteredData.map((group, index) => ( + !songs ? handleNavigate(group.id) : handleSelectSong(group)} + style={{ cursor: 'pointer' }} + > + + {group.title} + + ))} + + ); + return renderSwiper; + }, [songs]); // Add `songs` to dependencies to ensure memoization works correctly return ( @@ -50,27 +74,27 @@ export default function Carousel({ data, songs = false, genres }) { - - + + {genres.map((genre, index) => ( ))} - - {renderSwiper(data)} - - {genres.map(genre => ( + {memoizedSwiper(data)} + {genres.map((genre) => ( - {renderSwiper(data.filter(item => item.genre.key === genre.key))} + {memoizedSwiper(data.filter((item) => item.genre.key === genre.key))} ))} ) : ( - renderSwiper(data) + memoizedSwiper(data) )} ); diff --git a/qtify/src/components/Home/Home.css b/qtify/src/components/Home/Home.css new file mode 100644 index 0000000000..398c88866a --- /dev/null +++ b/qtify/src/components/Home/Home.css @@ -0,0 +1,40 @@ +.App { + text-align: center; + } + + .App-logo { + height: 40vmin; + pointer-events: none; + } + + @media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } + } + + .App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; + } + + .App-link { + color: #61dafb; + } + + @keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + \ No newline at end of file diff --git a/qtify/src/components/Home/Home.js b/qtify/src/components/Home/Home.js new file mode 100644 index 0000000000..9a3fb885af --- /dev/null +++ b/qtify/src/components/Home/Home.js @@ -0,0 +1,75 @@ +import './App.css'; +import Navbar from './components/Navbar/Navbar'; + +import Hero from './components/Hero/Hero'; +import axios from 'axios'; +import React, { useEffect, useState } from 'react'; +import Section from './components/Section/Section'; +import { Stack, Divider } from '@mui/material'; +import { MusicContext } from './MusicContext'; +import MusicBar from './components/MusicBar/MusicBar'; + + +function App() { + const [newData, setNewData] = useState([]) + const [topData, setTopData] = useState([]) + const [songData, setSongData] = useState([]) + const [genreData, setGenreData] = useState([]) + const [faqData, setFaqData] = useState([]) + + + const [selectedSong, setSelectedSong] = useState(null); + + + + + const fetchData = async (type) => { + try { + let res = await axios.get('https://qtify-backend-labs.crio.do/' + type) + + return res.data + } catch (e) { + console.log(e) + } + } + + useEffect(() => { + const getData = async () => { + let data = await fetchData('albums/new') + await setNewData(data) + data = await fetchData('albums/top') + await setTopData(data) + data = await fetchData('songs') + await setSongData(data) + data = await fetchData('genres') + await setGenreData(data.data) + data = await fetchData('faq') + await setFaqData(data.data) + + + } + getData() + + }, []) + + + + + return ( + +
+ + + }> +
+
+
+
+ + +
+
+ ); +} + +export default App; diff --git a/qtify/src/components/MusicBar/MusicBar.js b/qtify/src/components/MusicBar/MusicBar.js new file mode 100644 index 0000000000..142796abe1 --- /dev/null +++ b/qtify/src/components/MusicBar/MusicBar.js @@ -0,0 +1,104 @@ +import React from "react"; + +import { Box, Stack } from '@mui/material'; +import LinearProgress from '@mui/material/LinearProgress'; +import PlayCircleIcon from '@mui/icons-material/PlayCircle'; +import PauseCircleIcon from '@mui/icons-material/PauseCircle'; +import { MusicContext } from '../../MusicContext'; +import music from '../../assets/music.mp3'; + + +function MusicBar() { + + const { selectedSong, setSelectedSong } = React.useContext(MusicContext) + const [play, setPlay] = React.useState(false); + const [progress, setProgress] = React.useState(0); + const [isInitialRender, setIsInitialRender] = React.useState(true); + + const audioRef = React.useRef(null); + + + React.useEffect(() => { + let timer + + if (play) { + audioRef.current.play(); + timer = setInterval(() => { + setProgress((oldProgress) => { + if (oldProgress === 100) { + setPlay(false) + return 100 + } + return oldProgress + ( audioRef.current.duration/5000) + }) + }, [100]) + } else { + audioRef.current.pause(); + } + + + return () => { + clearInterval(timer); + }; + }, [play]) + + React.useEffect(() => { + if (isInitialRender) { + setIsInitialRender(false); + return; // Skip playback on initial render + } + if (selectedSong) { + setProgress(0) + setTimeout(() => { + setPlay(true) + }, [1000]) + } + }, [selectedSong]) + + const handlePlay = () => { + if (progress === 100) { + setProgress(0) + setTimeout(() => { + setPlay(!play) + }, [1000]) + } else { + setPlay(!play) + } + + } + + + return ( + <> +