Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: users can edit their generated playlist #19

Merged
merged 4 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/components/DiscoverTracks/SubmitButtion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,10 @@ const SubmitButtion = () => {
setLoadingMessage(`Getting all Artist's In The Playlist`);
const trackArtists = playlistTracks
.flat()
.map((item) => item.track.artists.slice(0, 2));
const artistNames = trackArtists.flat().map((item) => item.name);
.map((item: any) => item.track.artists.slice(0, 2));
const artistNames: string[] = trackArtists
.flat()
.map((item: any) => item.name);
const uniqueArtistNames = [...new Set(artistNames)];

getSimilarArtists(uniqueArtistNames);
Expand Down
197 changes: 176 additions & 21 deletions app/components/OpenOnSpotify.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,183 @@
import React, { useState } from 'react';

import { Icon } from '@iconify/react';
import React from 'react';
import Image from 'next/image';
import useViewPlaylist from '../hooks/useViewPlaylist';

function OpenOnSpotify({ link }: { link: string }) {
const {
showingTracks,
loading,
startedEditing,
tracksDeleted,
restoreSelectedTracks,
deleteTrack,
restoreAllTracks,
saveTracks,
} = useViewPlaylist(link);

const [restoreOption, setRestoreOption] = useState(false);
const [openPlaylist, setOpenPlaylist] = useState(false);
const [selectedTracksToRestore, setSelectedTracksToRestore] = useState<
string[]
>([]);

const handleClick = () => {
setOpenPlaylist(!openPlaylist);
};

const handleCheckboxChange = (id: string) => {
setSelectedTracksToRestore((prevSelectedTracks) => {
if (prevSelectedTracks.includes(id)) {
return prevSelectedTracks.filter((trackId) => trackId !== id);
} else {
return [...prevSelectedTracks, id];
}
});
};

const OpenOnSpotify = ({ link }: { link: string }) => {
return (
<div
className={`border-brand rounded border-2 px-4 py-3 flex flex-col gap-4`}>
<div className={`flex gap-1 ml-auto w-fit cursor-pointer`}>
<Icon icon='solar:copy-bold' className={`h-4 w-4`} />
<span className={`text-fxs md:text-f2xs`}>Copy Playlist Link</span>
</div>
<div className={`flex gap-1 items-center`}>
<Icon icon='logos:spotify-icon' width='30' height='30' />
<a
href={link}
target='_blank'
rel='norefferer'
className='flex flex-col'>
<span className={`text-fsm md:text-fxs`}>Open on Spotify</span>
<span className='underline text-brand whitespace-normal text-fxs md:text-f2xs -mt-1 text-ellipsis'>
Playlist generated by HearItFresh
</span>
</a>
<>
<div
className={`border-brand rounded border-2 px-4 py-3 flex flex-col gap-4`}>
<div className={`flex gap-1 ml-auto w-fit cursor-pointer`}>
<Icon icon='solar:copy-bold' className={`h-4 w-4`} />
<span className={`text-fxs md:text-f2xs`}>Copy Playlist Link</span>
</div>
<div className={`flex items-center justify-between`}>
<div className='flex gap-1 items-center'>
<Icon icon='logos:spotify-icon' width='30' height='30' />
<a
href={link}
target='_blank'
rel='norefferer'
className='flex flex-col'>
<span className={`text-fsm md:text-fxs`}>Open on Spotify</span>
<span className='underline text-brand whitespace-normal text-fxs md:text-f2xs -mt-1 text-ellipsis'>
Playlist generated by HearItFresh
</span>
</a>
</div>
<button
onClick={handleClick}
className='bg-brand rounded text-fsm px-4 py-2 text-lightest'>
View Playlist
</button>
</div>
</div>
</div>
{openPlaylist && (
<section className='h-screen w-screen fixed left-0 top-0 bg-slate-500 bg-opacity-60 z-50 flex items-center justify-center'>
<div className='mt-6 relative bg-lightest dark:bg-darkest w-[90%] sm:w-3/5 rounded p-6 flex flex-col max-h-[90%] min-w-[300px]'>
<div className='flex items-center justify-between sticky top-0 w-full text-fmd pr-2 pb-2'>
<p>Edit Playlist Generated</p>

<button onClick={handleClick} className='flex gap-1 items-center'>
<Icon icon='iconoir:cancel' width='20' height='20' />
<span>Close</span>
</button>
</div>
<ul className='h-96 overflow-y-scroll px-4 list-decimal flex flex-col gap-2'>
{showingTracks.map(({ id, name, artist, image }) => {
return (
<li key={id} className='flex items-center sm:gap-4 gap-2'>
<section className='flex sm:gap-4 gap-2 w-full pr-2 hover:opacity-45'>
<div className='sm:h-10 sm:w-10 h-7 w-7 relative rounded-md overflow-hidden'>
<Image
src={image as string}
alt={name + ' track image'}
fill
/>
</div>
<div className='flex-1 max-w-[100px] sm:max-w-full'>
<p className='text-fbase truncate'>{name}</p>
<p className='truncate text-fsm opacity-75 -mt-1'>
{artist.join(', ')}
</p>
</div>
</section>
<button
className='flex items-center gap-1 hover:bg-red-500 transition-all bg-red-600 bg-opacity-65 text-lightest rounded px-2.5 py-1.5 sm:text-fbase text-fsm'
onClick={() => deleteTrack(id)}>
<Icon icon='mdi:delete' width='18' height='18' />
<span>Delete</span>
</button>
</li>
);
})}
</ul>
{startedEditing && (
<div className='pt-4 flex flex-col'>
<div className='flex items-center gap-2 ml-auto'>
<button
onClick={() => setRestoreOption(!restoreOption)}
className='flex items-center gap-1 text-fsm sm:text-fbase'>
<span>Select Songs to Restore</span>
{restoreOption ? (
<Icon icon='mingcute:up-line' />
) : (
<Icon icon='mingcute:down-line' />
)}
</button>
<button
onClick={saveTracks}
disabled={loading.isLoading}
className='bg-brand px-4 py-1 rounded text-lightest text-fsm sm:text-fbase'>
{loading.isLoading ? loading.message : 'Save'}
</button>
</div>
{restoreOption && (
<section className='overflow-y-auto'>
{tracksDeleted.length > 0 && (
<div className='flex items-center gap-5'>
<button
onClick={restoreAllTracks}
className='border-brand border-2 px-4 py-1 rounded text-fsm mb-3 transition-all hover:bg-brand hover:text-lightest'>
Restore All
</button>
{selectedTracksToRestore.length > 0 && (
<button
className='border-brand border-2 px-4 py-1 rounded text-fsm mb-3 transition-all hover:bg-brand hover:text-lightest'
onClick={() => {
restoreSelectedTracks(selectedTracksToRestore);
setSelectedTracksToRestore([]);
}}>
Restore Selected
</button>
)}
</div>
)}
<div className='grid sm:grid-cols-4 grid-cols-3 gap-y-2 pl-2'>
{tracksDeleted.map(({ name, id, artist }) => {
return (
<label
key={id}
htmlFor={name}
className='flex items-center gap-2'>
<input
type='checkbox'
name={name}
id={id}
className='rounded'
onChange={() => handleCheckboxChange(id)}
/>
<div className='flex flex-col justify-between w-full pr-2'>
<p className='text-fsm'>{name}</p>
<p className='truncate text-fxs opacity-75'>
{artist.join(', ')}
</p>
</div>
</label>
);
})}
</div>
</section>
)}
</div>
)}
</div>
</section>
)}
</>
);
};

Expand Down
111 changes: 111 additions & 0 deletions app/hooks/useViewPlaylist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
getAllTracksInAPlaylist,
removeTracksFromPlaylists,
} from '../lib/spotify';
import { loadingType, playlistSongDetails } from '../types';
import { useEffect, useState } from 'react';

const useViewPlaylist = (
link: string,
): {
showingTracks: playlistSongDetails[];
loading: loadingType;
startedEditing: boolean;
tracksDeleted: playlistSongDetails[];
deleteTrack: (id: string) => void;
restoreAllTracks: () => void;
restoreSelectedTracks: (ids: string[]) => void;
saveTracks: () => void;
} => {
link = link.split('/').at(-1) as string;

const [loading, setLoading] = useState<loadingType>({
isLoading: false,
message: null,
});
const [startedEditing, setStartedEditing] = useState(false);
const [tracks, setTracks] = useState<playlistSongDetails[]>([]);
const [showingTracks, setShowingTracks] = useState<playlistSongDetails[]>([]);
const [tracksToRemove, setTracksToRemove] = useState<{ uri: string }[]>([]);
const [tracksDeleted, setTracksDeleted] = useState<playlistSongDetails[]>([]);

useEffect(() => {
(async () => {
await getTracks();
})();
}, []);

useEffect(() => {
if (showingTracks.length === 0) {
setShowingTracks(tracks);
}
}, [tracks]);

const getTracks = async () => {
const data = await getAllTracksInAPlaylist(link);

const tracks = data.map((item: any) => {
const track = item.track;
const image = track.album.images[1].url;
const artist = track.artists.map((subitem: any) => subitem.name);
return { id: track.id, name: track.name, artist, image };
});

setTracks(tracks);
};

const deleteTrack = (id: string) => {
setStartedEditing(true);
setShowingTracks((prevTracks) =>
prevTracks.filter((track) => track.id !== id),
);
const deletingTrack = tracks.filter((track) => track.id === id)[0];
setTracksDeleted([...tracksDeleted, deletingTrack]);
setTracksToRemove([...tracksToRemove, { uri: 'spotify:track:' + id }]);
};

const restoreAllTracks = () => {
setShowingTracks(tracks);
setStartedEditing(false);
setTracksToRemove([]);
setTracksDeleted([]);
};

const restoreSelectedTracks = (ids: string[]) => {
const restoringTracks = tracks.filter((track) => ids.includes(track.id));
const remainingDeletedTracks = tracksDeleted.filter(
(track) => !ids.includes(track.id),
);
const TracksToRemove = tracksToRemove.filter((track) =>
ids.includes(track.uri.split(':').at(-1) as string),
);

setTracksToRemove(TracksToRemove);
setTracksDeleted(remainingDeletedTracks);
setShowingTracks([...showingTracks, ...restoringTracks]);
};

const saveTracks = async () => {
setLoading({ isLoading: true, message: 'Deleting Tracks....' });
removeTracksFromPlaylists(link, tracksToRemove);

await getTracks();
setLoading({ isLoading: false, message: null });
setTracksToRemove([]);
setTracksDeleted([]);
setStartedEditing(false);
};

return {
showingTracks,
loading,
startedEditing,
deleteTrack,
restoreAllTracks,
saveTracks,
restoreSelectedTracks,
tracksDeleted,
};
};

export default useViewPlaylist;
Loading