import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { collectionStore } from '../../stores'; import { deletePlaylistFromWNFS, type Playlist } from '../../lib/playlists'; import { ipfsGatewayUrl } from '../../../../lib/app-info'; import Download from '../../../../components/icons/Download'; import Trash from '../../../../components/icons/Trash'; type Props = { playlist: Playlist; isModalOpen: boolean; onClose: () => void; }; const PlaylistModal = ({ playlist, isModalOpen, onClose }: Props) => { const collection = useRecoilValue(collectionStore); const [selectedPlaylist, setSelectedPlaylist] = useState<Playlist | null>( playlist ); const [openModal, setOpenModal] = useState<boolean>(isModalOpen); const [previousPlaylist, setPreviousPlaylist] = useState<Playlist | null>(); const [nextPlaylist, setNextPlaylist] = useState<Playlist | null>(); const [showPreviousArrow, setShowPreviousArrow] = useState<boolean>(); const [showNextArrow, setShowNextArrow] = useState<boolean>(); /** * Close the modal, clear the playlist state vars, set `isModalOpen` to false * and dispatch the close event to clear the playlist from the parent's state */ const handleCloseModal: () => void = () => { setPreviousPlaylist(null); setNextPlaylist(null); setSelectedPlaylist(null); setOpenModal(false); onClose(); }; /** * Delete an playlist from the user's WNFS */ const handleDeletePlaylist: () => Promise<void> = async () => { if (selectedPlaylist) { await deletePlaylistFromWNFS(selectedPlaylist.name); handleCloseModal(); } }; /** * Set the previous and next playlists to be toggled to when the arrows are clicked */ const setCarouselState = () => { const playlistList = selectedPlaylist?.private ? collection.privatePlaylists : collection.publicPlaylists; const currentIndex = playlistList.findIndex( (val) => val.cid === selectedPlaylist?.cid ); const updatedPreviousPlaylist = playlistList[currentIndex - 1] ?? playlistList[playlistList.length - 1]; setPreviousPlaylist(updatedPreviousPlaylist); const updatedNextPlaylist = playlistList[currentIndex + 1] ?? playlistList[0]; setNextPlaylist(updatedNextPlaylist); setShowPreviousArrow(playlistList.length > 1 && !!updatedPreviousPlaylist); setShowNextArrow(playlistList.length > 1 && !!updatedNextPlaylist); }; /** * Load the correct playlist when a user clicks the Next or Previous arrows * @param direction */ const handleNextOrPrevPlaylist: (direction: 'next' | 'prev') => void = ( direction ) => { setSelectedPlaylist( (direction === 'prev' ? previousPlaylist : nextPlaylist) ?? null ); }; useEffect(() => { setCarouselState(); }, [selectedPlaylist]); /** * Detect `Escape` key presses to close the modal or `ArrowRight`/`ArrowLeft` * presses to navigate the carousel * @param event */ const handleKeyDown: (event: KeyboardEvent) => void = (event) => { if (event.key === 'Escape') handleCloseModal(); if (showNextArrow && event.key === 'ArrowRight') handleNextOrPrevPlaylist('next'); if (showPreviousArrow && event.key === 'ArrowLeft') handleNextOrPrevPlaylist('prev'); }; // Attach attach left/right/esc keys to modal actions useEffect(() => { window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [handleKeyDown]); const useMountEffect = () => useEffect(() => { setCarouselState(); }, []); useMountEffect(); if (selectedPlaylist) { return ( <> <input type="checkbox" id={`playlist-modal-${selectedPlaylist.cid}`} className="modal-toggle" checked={openModal} onChange={() => undefined} /> <label htmlFor={`playlist-modal-${selectedPlaylist.cid}`} className="z-50 cursor-pointer modal" onClick={(event) => { if (event.currentTarget !== event.target) { return; } handleCloseModal(); }} > <div className="relative text-center modal-box text-base-content"> <label htmlFor={`playlist-modal-${selectedPlaylist.cid}`} className="absolute btn btn-xs btn-circle right-2 top-2" onClick={handleCloseModal} > ✕ </label> <div> <h3 className="text-lg break-all mb-7"> {selectedPlaylist.name} </h3> <div className="relative"> {showPreviousArrow && ( <button className="absolute top-1/2 -left-[25px] -translate-y-1/2 inline-block text-center text-[40px]" onClick={() => handleNextOrPrevPlaylist('prev')} > ‹ </button> )} <img className="block object-cover object-center border-2 border-base-content w-full h-full mb-4 rounded-[1rem]" alt={selectedPlaylist.name} src={selectedPlaylist.src} /> {showNextArrow && ( <button className="absolute top-1/2 -right-[25px] -translate-y-1/2 inline-block text-center text-[40px]" onClick={() => handleNextOrPrevPlaylist('next')} > › </button> )} </div> <div className="flex flex-col items-center justify-center"> <p className="mb-2 text-neutral-500"> Created {new Date(selectedPlaylist.ctime).toDateString()} </p> <a href={`https://ipfs.${ipfsGatewayUrl}/ipfs/${selectedPlaylist.cid}/userland`} target="_blank" rel="noreferrer" className="mb-2 underline hover:text-neutral-500" > View on IPFS{selectedPlaylist.private && `*`} </a> {selectedPlaylist.private && ( <> <p className="mb-2 text-neutral-700 dark:text-neutral-500"> * Your private files can only be viewed on devices that have permission. When viewed directly on IPFS, you will see the encrypted state of this file. This is because the raw IPFS gateway view does not have permission to decrypt this file. </p> <p className="mb-2 text-neutral-700 dark:text-neutral-500"> Interested in private file sharing as a feature? Follow the{' '} <a className="underline" href="https://github.com/oddsdk/odd-app-template/issues/4" target="_blank" rel="noreferrer" > github issue. </a> </p> </> )} <div className="flex flex-col items-center justify-between gap-4 mt-4 sm:flex-row"> <a href={selectedPlaylist.src} download={selectedPlaylist.name} className="gap-2 btn btn-primary" > <Download /> Download Playlist </a> <button className="gap-2 btn btn-outline" onClick={handleDeletePlaylist} > <Trash /> Delete Playlist </button> </div> </div> </div> </div> </label> </> ); } return null; }; export default PlaylistModal;