Skip to content

Commit

Permalink
Merge pull request #29 from ECE651-ReWrapped/persist-user-email
Browse files Browse the repository at this point in the history
Create and view collaborative playlists with users
  • Loading branch information
samudra-perera authored Mar 30, 2024
2 parents 12740e5 + 0e3a4d8 commit 15f8452
Show file tree
Hide file tree
Showing 12 changed files with 283 additions and 14 deletions.
5 changes: 5 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import UserProfile from "./pages/Profile";
import SetNewPassword from "./pages/SetNewPassword";
import CreatePlaylist from "./pages/CreatePlaylist";
import Recommendations from "./pages/Recommendations"
import SelectedPlaylist from "./components/SelectedPlaylist";

const router = createBrowserRouter([
{
Expand Down Expand Up @@ -66,6 +67,10 @@ const router = createBrowserRouter([
<Recommendations />
</ProtectedRoute>
)
},
{
path: "/my-playlists/:playlist_name",
element: <SelectedPlaylist />
}
]);

Expand Down
85 changes: 77 additions & 8 deletions src/components/PlaylistDialog.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,90 @@
import React from 'react';
import { Dialog, DialogTitle, DialogContent, ListItemButton, List, ListItemText } from '@mui/material';
import { Dialog, DialogTitle, DialogContent, ListItemButton, List, ListItemText, Button, TextField } from '@mui/material';
import { useState, useEffect } from 'react';
import axios from 'axios';
import { useSelector } from "react-redux";
import { useNavigate } from 'react-router-dom';

const PlaylistDialog = ({ handleCloseList }) => {
// Dummy list of playlist names
const playlists = ['Playlist 1', 'Playlist 2', 'Playlist 3', 'Playlist 4', 'Playlist 5'];
let playlists = [];

const PlaylistDialog = ({ currUser, handleCloseList }) => {
const currUserEmail = useSelector(state => state.currentUserDetails.userEmail);
const [isPlaylistEmpty, setPlaylistEmpty] = useState(true); // consider no shared playlists until API call sends a list
const [enterPlaylist, setEnterPlaylist] = useState(false);
const [playlistName, setPlaylistName] = useState("");
const navigate = useNavigate();

useEffect(() => {
const fetchPlaylistsFromDb = async () => {
playlists = [];
try {
const res = await axios.get(`${process.env.REACT_APP_API_LOCAL}/getSharedPlaylists`, {
params: {
createdByUserEmail: currUserEmail,
sharedWithUsername: currUser
},
withCredentials: true,
});

if (res.status === 404) {
// no existing shared playlists
setPlaylistEmpty(true);
} else {
setPlaylistEmpty(false);
// display existing playlists
res.data.playlists.forEach((item) => {
playlists.push(item.playlist_name);
})
}
} catch (err) {
console.error("Failed to fetch playlists: ", err);
}
};
fetchPlaylistsFromDb();
}, [currUserEmail, isPlaylistEmpty]);

const handleCreatePlaylist = () => {
setEnterPlaylist(true);
}

const onDone = async () => {
setEnterPlaylist(false);
setPlaylistEmpty(false);
handleCloseList(false); // close dialog box altogether

try {
const res = await axios.post(
`${process.env.REACT_APP_API_LOCAL}/createNewSharedPlaylist`,
{
playlist_name: playlistName,
createdByEmail: currUserEmail,
sharedWithUsername: currUser
}
);
} catch (err) {
console.error("Failed to create a new playlist: ", err);
}
};

// display playlist tracks in the selected playlist to user
const handleGetPlaylistTracks = async (selectedPlaylist) => {
navigate(`/my-playlists/${selectedPlaylist}`);
};

return (
<Dialog open={true} onClose={() => handleCloseList(false)}>
<DialogTitle>Add a song to your playlist</DialogTitle>
{!isPlaylistEmpty && !enterPlaylist && <DialogTitle>Your shared playlists</DialogTitle>}
<DialogContent>
<List>
{!isPlaylistEmpty && !enterPlaylist && <List>
{playlists.map((playlist, index) => (
<ListItemButton onClick={() => handleCloseList(false)} key={index}>
<ListItemButton onClick={() => { handleGetPlaylistTracks(playlist); }} key={index}>
<ListItemText primary={playlist} />
</ListItemButton>
))}
</List>
</List>}
{enterPlaylist && <TextField value={playlistName} onChange={(e) => setPlaylistName(e.target.value)} id="outlined-basic" label="Outlined" variant="outlined" />}
{enterPlaylist && <Button onClick={onDone}>Done</Button>}
{!enterPlaylist && <Button onClick={handleCreatePlaylist}>Create a new Playlist</Button>}
</DialogContent>
</Dialog>
);
Expand Down
8 changes: 4 additions & 4 deletions src/components/RecommendationCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import ShareIcon from '@mui/icons-material/Share';
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import { Tooltip, Box } from '@mui/material';
import { useState } from 'react';
import PlaylistDialog from "./PlaylistDialog";
import SongToPlaylistDialog from './SongToPlaylistDialog';

const RecommendationCard = (props) => {
const [addToPlaylist, setAddToPlaylist] = useState(false);
const [addSongToPlaylist, setAddSongToPlaylist] = useState(false);

const handleAddToPlaylist = () => {
setAddToPlaylist(true);
setAddSongToPlaylist(true);
};

return (
Expand Down Expand Up @@ -55,7 +55,7 @@ const RecommendationCard = (props) => {
<PlaylistAddIcon />
</IconButton>
</Tooltip>
{addToPlaylist && <PlaylistDialog handleCloseList={setAddToPlaylist} />}
{addSongToPlaylist && <SongToPlaylistDialog songName={props.nameOfSong} songArtist={props.nameOfArtist} handleCloseDialog={setAddSongToPlaylist} />}
</CardActions>
</Card>
</Box>
Expand Down
10 changes: 9 additions & 1 deletion src/components/SearchCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import axios from "axios";
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useEffect, useState } from 'react';
import PlaylistDialog from "./PlaylistDialog";

// Call this function to display a toast with the error message
const notify = (message) => toast.error(message);

const SearchCard = ({ user }) => {
const [isFollowed, setIsFollowed] = useState(false);
const [ viewPlaylistDialog, setViewPlaylistDialog ] = useState(false);

useEffect(() => {
const checkFollowStatus = async () => {
Expand Down Expand Up @@ -58,6 +60,11 @@ const SearchCard = ({ user }) => {
}
};

// shared playlists handlers
const handleAddPlaylist = async () => {
setViewPlaylistDialog(true);
};

return (
<>
<ToastContainer position="top-right" autoClose={5000} hideProgressBar={false} newestOnTop={false} closeOnClick rtl={false} pauseOnFocusLoss draggable pauseOnHover />
Expand All @@ -77,9 +84,10 @@ const SearchCard = ({ user }) => {
<Button onClick={isFollowed ? unfollowUser : followUser}>
{isFollowed ? 'Unfollow' : 'Follow'}
</Button>
<Button>Add to Playlist</Button>
<Button onClick={handleAddPlaylist}>Add Playlist</Button>
</Box>
</Card>
{viewPlaylistDialog && <PlaylistDialog currUser={user.user_name} handleCloseList={setViewPlaylistDialog} />}
</>
);
};
Expand Down
80 changes: 80 additions & 0 deletions src/components/SelectedPlaylist.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useParams } from 'react-router-dom';
import React from 'react';
import { useEffect, useState } from 'react';
import { List, ListItem, Divider, ListItemText, ListItemAvatar, Avatar, Box, Typography } from '@mui/material';
import axios from 'axios';

const SelectedPlaylist = () => {
const { playlist_name } = useParams();
const [tracks, setTracks] = useState([]);

useEffect(() => {
const fetchApiData = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_API_LOCAL}/getAllTracksFromPlaylist`, {
params: {
playlist_name: playlist_name
},
withCredentials: true,
});

if (res.status === 200) {
if (res.data.tracks.length === 0) {
setTracks([]);
} else {
const formattedTracks = res.data.tracks.map((item) => ({
track: item.track_name,
artist: item.artist_name
}));
setTracks(formattedTracks);
}
} else {
console.log("Failed to get tracks from playlist.");
}
} catch (err) {
console.error("Failed to get tracks from playlist: ", err);
}
};

fetchApiData();
}, [playlist_name]);

return (
<>
<Typography
padding="5px"
style={{ fontFamily: "sans-serif", fontWeight: 'bold', textAlign: 'center' }}
variant="h4"
marginTop="30px"
gutterBottom
>
Songs in this playlist
</Typography>
{tracks.length > 0 ? (
<Box sx={{ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", marginTop: '30px' }}>
<List sx={{ width: '100%', maxWidth: 500, bgcolor: '#F0F2F5' }}>
{tracks.map((item, index) => (
<React.Fragment key={index}>
<ListItem alignItems="flex-start">
<ListItemAvatar sx={{ padding: '10px' }}>
<Avatar alt={item.artist} src="/static/images/avatar/1.jpg" />
</ListItemAvatar>
<ListItemText
primary={item.track}
secondary={item.artist}
sx={{ padding: '10px' }}
/>
</ListItem>
<Divider variant="inset" component="li" />
</React.Fragment>
))}
</List>
</Box>
) : (
<p>This playlist has no tracks!</p>
)}
</>
);
};

export default SelectedPlaylist;
75 changes: 75 additions & 0 deletions src/components/SongToPlaylistDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { Button, Dialog, DialogTitle, List, ListItemButton, ListItemText, DialogContent } from '@mui/material';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import axios from 'axios';

let playlists = [];

const SongToPlaylistDialog = ({ songName, songArtist, handleCloseDialog }) => {
const currUserEmail = useSelector(state => state.currentUserDetails.userEmail);
const [hasPlaylists, setHasPlaylists] = useState(false);

useEffect(() => {
// get all of my playlists only, shared or unshared
const getMyPlaylists = async () => {
playlists = [];
try {
const response = await axios.get(`${process.env.REACT_APP_API_LOCAL}/getSharedPlaylists`, {
params: {
createdByUserEmail: currUserEmail,
sharedWithUsername: undefined // don't set to get all of only my playlists
},
withCredentials: true,
});

if (response.status === 200) {
// there are playlists to my name
response.data.playlists.map((item) => {
playlists.push(item.playlist_name);
});
setHasPlaylists(true);
} else {
// I dont have any shared playlists
}

} catch (err) {
console.error("Failed to get user's playlists: ", err);
}
};
getMyPlaylists();
}, []);

const handleAddToChosenPlaylist = (selPlaylist) => {
handleCloseDialog(false);
setHasPlaylists(false);
// store this song to the selected playlist in db
try {
const res = axios.post(`${process.env.REACT_APP_API_LOCAL}/addTrackToPlaylist`, {
playlist_name: selPlaylist,
track_name: songName,
artist_name: songArtist
});
} catch (err) {
console.error("Failed to add track to the playlist: ", err);
}
};

return (
<Dialog open={true} onClose={() => handleCloseDialog(false)}>
<DialogTitle>Choose a playlist to add this song to</DialogTitle>
<DialogContent>
<List>
{playlists.map((playlist, index) => (
<ListItemButton key={index} onClick={ () => handleAddToChosenPlaylist(playlist) }>
<ListItemText primary={playlist} />
</ListItemButton>
))}
</List>
<Button onClick={() => handleCloseDialog(false)}>Done</Button>
</DialogContent>
</Dialog>
)
};

export default SongToPlaylistDialog;
1 change: 1 addition & 0 deletions src/containers/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const Login = () => {
);

if (res.status === 200) {
sessionStorage.setItem('currentUserEmail', values.email); // to make it persist across browser refresh
window.location.href = 'http://localhost:6001/loginSpotify';
} else {
// Handle non-200 HTTP status codes if needed
Expand Down
6 changes: 6 additions & 0 deletions src/containers/Signup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import { validationSchema } from "../utility/passwordValidator";
// import { useAxios } from "../hooks/useAxios";
// import { passwordValidation } from "../utility/passwordValidator";
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import { useDispatch } from "react-redux";
import { userDetailsActions } from "../slices/user/user-details-slice";

const Signup = () => {
const navigate = useNavigate();
const dispatch = useDispatch();

const createUser = async (values) => {
try {
const res = await axios.post(
Expand All @@ -29,6 +33,8 @@ const Signup = () => {
console.log(res);

if (res.status === 200) {
// store current user's email as global state
dispatch(userDetailsActions.setUserEmail(values.email));
navigate("/dashboard");
} else {
// Handle non-200 HTTP status codes if needed
Expand Down
7 changes: 7 additions & 0 deletions src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import ListeningTrendsGraph from "../components/listeningTrendsGraph";
import TopGenresGraph from "../components/topGenresGraph";
import StatsCard from "../components/statsCard";
import TopSongCard from "../components/TopSongCard"
import { useDispatch } from "react-redux";
import { userDetailsActions } from "../slices/user/user-details-slice";
// Supports weights 100-900
// todo: temp data until backend is done

Expand Down Expand Up @@ -74,9 +76,14 @@ const sampleSongData = [
const sampleListeningData = [];

function Dashboard() {
const dispatch = useDispatch();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);

// get user's email from localStorage
const currUserEmail = sessionStorage.getItem('currentUserEmail');
dispatch(userDetailsActions.setUserEmail(currUserEmail));

const name = queryParams.get("displayName");

const [rpData, setData] = useState([sampleSongData]);
Expand Down
Loading

0 comments on commit 15f8452

Please sign in to comment.