diff --git a/client/src/Playlist-Editor/Chat/ChatMessage.tsx b/client/src/Playlist-Editor/Chat/ChatMessage.tsx index 62d34f7..1d88b63 100644 --- a/client/src/Playlist-Editor/Chat/ChatMessage.tsx +++ b/client/src/Playlist-Editor/Chat/ChatMessage.tsx @@ -69,11 +69,15 @@ export const SituatedChatMessage = ({ {new Date(chatEvent.timestamp).toLocaleString()} - { chatEvent.action && + { chatEvent.action !== 'comment' &&

{ chatEvent.action === 'add' ? 'Added this track' - : 'Removed this track' + : chatEvent.action === 'remove' + ? 'Removed this track' + : chatEvent.action === 're-add' + ? 'Re-added this track' + : 'Error occurred' }

} diff --git a/client/src/Playlist-Editor/Chat/MessageEditor.tsx b/client/src/Playlist-Editor/Chat/MessageEditor.tsx index 504252e..76300b1 100644 --- a/client/src/Playlist-Editor/Chat/MessageEditor.tsx +++ b/client/src/Playlist-Editor/Chat/MessageEditor.tsx @@ -52,11 +52,13 @@ type UserAction = State['userAction'] const iconOfAction = (action: UserAction) => - action === 'add' + action === 'add' || action === 're-add' ? faPlusCircle : action === 'remove' ? faMinusCircle - : faPaperPlane + : action === 'view' + ? faPaperPlane + : null @@ -66,7 +68,7 @@ export const SituatedMessageEditor = ({ onSubmit, onCancel, }: { - action: UserAction, //'add' | 'remove' | 'view' + action: UserAction, onSubmit: (message: string) => Promise, onCancel: () => void, }) => { @@ -94,12 +96,16 @@ export const SituatedMessageEditor = ({ ? 'Add' : action === 'remove' ? 'Remove' + : action === 're-add' + ? 'Re-add' : 'Post' // send? submit? comment? const placeholderText = action === 'add' ? 'Explain why you want to add this track... (optional)' : action === 'remove' ? 'Explain why you want to remove this track... (optional)' + : action === 're-add' + ? 'Explain why you want to add this track back... (optional)' : 'Comment on this track...' return <> diff --git a/client/src/Playlist-Editor/Chat/SituatedChat.tsx b/client/src/Playlist-Editor/Chat/SituatedChat.tsx index ab63bce..28df403 100644 --- a/client/src/Playlist-Editor/Chat/SituatedChat.tsx +++ b/client/src/Playlist-Editor/Chat/SituatedChat.tsx @@ -4,8 +4,8 @@ import { classes } from '../../styles' import * as styles from '../playlistTableRowStyles' import { State } from '../modificationReducer' import { SituatedMessageEditor } from './MessageEditor' -import { PlaylistTrackObject } from '../../shared/apiTypes' import { SituatedChatMessage } from './ChatMessage' +import { SituatedChatEvent } from '../../shared/dbTypes' const chatStyle = { @@ -16,13 +16,13 @@ const chatStyle = { export const SituatedChat = ({ - track, + chat, action, onSubmit, onCancel, }: { - track: PlaylistTrackObject, - action: State['userAction'], //'add' | 'remove' | 'view' + chat: SituatedChatEvent[], + action: State['userAction'], onSubmit: (message: string) => Promise, onCancel: () => void, }) => { @@ -35,7 +35,7 @@ export const SituatedChat = ({
- { track.chat.map((chatEvent, index) => + { chat.map((chatEvent, index) => ) }
diff --git a/client/src/Playlist-Editor/DraftAdditionSongRow.tsx b/client/src/Playlist-Editor/DraftAdditionSongRow.tsx index e4f02b9..0a413fd 100644 --- a/client/src/Playlist-Editor/DraftAdditionSongRow.tsx +++ b/client/src/Playlist-Editor/DraftAdditionSongRow.tsx @@ -12,6 +12,7 @@ import { PlaylistTrackObject, PostTrackRequest } from '../shared/apiTypes' import { handleApiError } from '../api' import { postWrapper } from '../fetchWrapper' import { useParams } from 'react-router' +import { asType } from '../util' /** @@ -28,12 +29,9 @@ export const DraftAdditionSongRow = ({ const track: PlaylistTrackObject = { ...trackData, chat: [], - removed: false, addedBy: 'You', // supposed to be an id, idk whether to use user's id } - const artistNames = track.artists.map(artist => artist.name).join(', ') - const { setModificationState, loadPlaylist } = useContext(playlistContext) const { id: playlistId } = useParams() @@ -42,10 +40,10 @@ export const DraftAdditionSongRow = ({ const onSubmit = async (message: string) => { const response = await postWrapper( `/api/playlists/${playlistId}/tracks`, - { + asType({ message, trackId: track.id - } as PostTrackRequest, + }), ) handleApiError(response) @@ -80,8 +78,8 @@ export const DraftAdditionSongRow = ({
{track.name}
-
{artistNames}
-
{track.album.name}
+
{track.artists}
+
{track.album}
{track.addedBy}
{ @@ -78,6 +79,7 @@ const usePlaylistData = (playlistId: string) => { +const padding = '2.0rem' const searchTabStyle = { flex: 0.2, @@ -95,12 +97,23 @@ const playlistTableStyle = { } const tHeadStyle = { background: colors.grayscale.gray, - padding: '2.0rem 2.0rem 0', + padding: `${padding} ${padding} 0`, position: 'sticky', top: 0, } as const const songsStyle = { - padding: '0 2.0rem 2.0rem', + padding: `0 ${padding}`, +} +const removedHeaderStyle = { + padding: `4.0rem ${padding} 0`, +} +const removedHeadingStyle = { + ...classes.text, + ...classes.bold, + fontSize: '2.4rem', +} +const bottomSpaceStyle: CSSProperties = { + minHeight: '5.0rem', } const separateChatStyle = { flex: 1, @@ -157,6 +170,18 @@ export const PlaylistEditor = ({ }
+ { playlist.data.removedTracks.length && <> +
+

Removed Tracks

+ +
+
+ { playlist.data.removedTracks.map(track => + + )} +
+ } +
}
{ false && {playlist.name}

{playlist.name}

-

- Created by{' '} - - {playlist.owner.display_name} - +

+ + {playlist.followers} + {' '} + Follower{playlist.followers !== 1 && 's'}

- - - {playlist.followers.total} - {' '} - Follower{playlist.followers.total !== 1 && 's'} -
} diff --git a/client/src/Playlist-Editor/PlaylistTableHeader.tsx b/client/src/Playlist-Editor/PlaylistTableHeader.tsx index b6d1f1d..df0cf3c 100644 --- a/client/src/Playlist-Editor/PlaylistTableHeader.tsx +++ b/client/src/Playlist-Editor/PlaylistTableHeader.tsx @@ -28,3 +28,22 @@ export const PlaylistTableHeader = () => { } +export const RemovedTracksHeader = () => { + return
+
+
+ Title +
+
+ Artist +
+
+ Album +
+
+ Removed by +
+
+
+} + diff --git a/client/src/Playlist-Editor/RemovedTrackRow.tsx b/client/src/Playlist-Editor/RemovedTrackRow.tsx new file mode 100644 index 0000000..1b8977a --- /dev/null +++ b/client/src/Playlist-Editor/RemovedTrackRow.tsx @@ -0,0 +1,145 @@ + +import React, { useContext, useState } from 'react' +import { useParams } from 'react-router' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faChevronCircleDown, faChevronCircleUp, faPlusCircle } from '@fortawesome/free-solid-svg-icons' +import { handleApiError } from '../api' +import { postWrapper } from '../fetchWrapper' +import { PostSituatedChatRequest, PutTrackRemovedRequest } from '../shared/apiTypes' +import { RemovedTrackObject } from '../shared/dbTypes' +import { classes, colors } from '../styles' +import { useHover } from '../useHover' +import { asType } from '../util' +import { playlistContext } from './playlistContext' +import * as styles from './playlistTableRowStyles' +import { SituatedChat } from './Chat/SituatedChat' + + +export const RemovedTrackRow = ({ track }: { track: RemovedTrackObject }) => { + + const { + modificationState, setModificationState, loadPlaylist + } = useContext(playlistContext) + + const { id: playlistId } = useParams() + + // true if user is attempting to re-add *this* track; else the user is just + // viewing or commenting on this track + const reAddingThis = modificationState.userAction === "re-add" + && modificationState.trackId === track.id + + // situated chat expand/collapse button + const [chatExpanded, setChatExpanded] = useState(false) + + // button hover states + const [expandButtonIsHovered, expandButtonHoverProps] = useHover() + const [reAddButtonIsHovered, reAddButtonHoverProps, setReAddButtonIsHovered] = useHover() + + + // on click the right-most "re-add" button + const reAddButtonOnClick = () => { + setModificationState({ + userAction: 're-add', + trackId: track.id, + }) + setReAddButtonIsHovered(false) // otherwise, stays hovered if cancelled + } + + // on submit the chat form + const onSubmitChat = async (message: string) => { + const response = reAddingThis + ? await postWrapper( + `/api/playlists/${playlistId}/tracks/${track.id}/removed/`, + asType({ + remove: false, + message + }), + { method: 'PUT' } + ) + : await postWrapper( + `/api/playlists/${playlistId}/tracks/${track.id}/chat/`, + asType({ message }), + ) + handleApiError(response) + + if (!response.error) { + // just resets modification state to 'view' + setModificationState({ userAction: 'view' }) + // reload playlist to get updated tracks/chats + loadPlaylist() + // inform caller of success; this clears the textarea + return true + } + return false + } + // on cancel the chat form + const onCancelChat = () => { + if (reAddingThis) { + setModificationState({ userAction: 'view' }) + } else { + setChatExpanded(false) + } + } + + + + const expandButtonStyle = { + ...styles.rightButtonStyle, + background: colors.translucentWhite(expandButtonIsHovered ? 0.3 : 0.15), + } + const rightButtonStyle = { + ...styles.rightButtonStyle, + background: colors.translucentWhite(reAddButtonIsHovered ? 0.3 : 0.15), + } + + return
+
+
+ {/* only show expand/collapse button if not currently re-adding this track */} + { !reAddingThis && + + } +
+
{track.name}
+
{track.artists}
+
{track.album}
+
{track.removedBy}
+
+ {/* only provide the re-add button as an option if no other track is + currently selected for modification */} + { modificationState.userAction === "view" && + + } +
+
+ {/* only show chat if this track is selected for re-adding or chat is expanded */} + { (reAddingThis || chatExpanded) && + + } +
+} + diff --git a/client/src/Playlist-Editor/SavedSongRow.tsx b/client/src/Playlist-Editor/SavedSongRow.tsx index 26f7e5a..1b6b60c 100644 --- a/client/src/Playlist-Editor/SavedSongRow.tsx +++ b/client/src/Playlist-Editor/SavedSongRow.tsx @@ -11,6 +11,7 @@ import { PlaylistTrackObject, PostSituatedChatRequest, PutTrackRemovedRequest } import { useParams } from 'react-router' import { postWrapper } from '../fetchWrapper' import { handleApiError } from '../api' +import { asType } from '../util' @@ -27,7 +28,6 @@ export const SavedSongRow = ({ addedByUsers: Record, }) => { - const artistNames = track.artists.map(artist => artist.name).join(', ') const addedByUser = addedByUsers[track.addedBy] const { @@ -63,12 +63,15 @@ export const SavedSongRow = ({ const response = removingThis ? await postWrapper( `/api/playlists/${playlistId}/tracks/${track.id}/removed/`, - { message } as PutTrackRemovedRequest, + asType({ + remove: true, + message + }), { method: 'PUT' } ) : await postWrapper( `/api/playlists/${playlistId}/tracks/${track.id}/chat/`, - { message } as PostSituatedChatRequest, + asType({ message }), ) handleApiError(response) @@ -124,8 +127,8 @@ export const SavedSongRow = ({ }
{track.name}
-
{artistNames}
-
{track.album.name}
+
{track.artists}
+
{track.album}
{addedByUser.display_name}
{/* only provide the remove button as an option if no other track is @@ -144,7 +147,7 @@ export const SavedSongRow = ({ {/* only show chat if this track is selected for removal or chat is expanded */} { (removingThis || chatExpanded) && => { - const [resource, setter] = useResource(null) +export const useSongSearch = (query: string) => { + const [resource, setter] = useResource(null) useEffect(() => { if (query !== '') { @@ -18,7 +17,7 @@ export const useSongSearch = (query: string): Resource => { setter({ loading: true, }) - const response = await fetchWrapper(`/api/search?q=${query}`) + const response = await fetchWrapper(`/api/search?q=${query}`) handleApiError(response) setter({ loading: false, @@ -82,7 +81,7 @@ export const SearchPanel = ({ const [query, setQuery] = useState('') - const { data: result } = useSongSearch(query) + const searchResults = useSongSearch(query) const searchTabStyle = { ...style, @@ -112,10 +111,10 @@ export const SearchPanel = ({ placeholder="Search to add track..." /> - { result && + { searchResults.data && } diff --git a/client/src/Playlist-Editor/SearchResults.tsx b/client/src/Playlist-Editor/SearchResults.tsx index e036f71..22a3111 100644 --- a/client/src/Playlist-Editor/SearchResults.tsx +++ b/client/src/Playlist-Editor/SearchResults.tsx @@ -6,26 +6,25 @@ import { faPlusCircle } from '@fortawesome/free-solid-svg-icons' import { useHover } from '../useHover' import { playlistContext } from './playlistContext' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { GetTrackSearchItem, GetTrackSearchResponse } from '../shared/apiTypes' +const itemNotFirstStyle = { + marginTop: '1.0rem', +} export const SearchResults = ({ - data, + searchResults, style, }: { - data: SpotifyApi.TrackSearchResponse, + searchResults: GetTrackSearchResponse, style?: CSSProperties, }) => { - const items = data.tracks.items - - const itemNotFirstStyle = { - marginTop: '1.0rem', - } return - {items.map((item, index) => + {searchResults.map((track, index) => @@ -69,29 +68,22 @@ const addButtonStyle = { } as const const SearchItem = ({ - item, + track, style, }: { - item: SpotifyApi.TrackObjectFull, + track: GetTrackSearchItem, style: CSSProperties, }) => { - const { name, artists, album } = item - const image = album.images[2] - const artistNames = artists.map(artist => artist.name).join(', ') const { modificationState, setModificationState } = useContext(playlistContext) const [addButtonIsHovered, addButtonHoverProps, setAddButtonIsHovered] = useHover() const addButtonOnClick = () => { + const { id, album, artists, name } = track setModificationState({ userAction: 'add', - trackData: { - id: item.id, - album: item.album, - artists: item.artists, - name: item.name, - } + trackData: { id, album, artists, name }, }) setAddButtonIsHovered(false) // otherwise, stays hovered if addition is cancelled } @@ -106,10 +98,10 @@ const SearchItem = ({ } return
- {`Album: + {`Album:
-
{name}
-
{artistNames}
+
{track.name}
+
{track.artists}
{ modificationState.userAction === 'view' &&