From e79850ac2aec67cf26fb9b3e06d990ca074b085a Mon Sep 17 00:00:00 2001 From: Jonathan Hooth Date: Sun, 22 Feb 2026 16:47:32 -0500 Subject: [PATCH 1/4] Track Search works --- src/api/ApiClient.ts | 42 +++++++++++++++---- src/apps/admin/pages/JukeSession.tsx | 18 +++++++- src/apps/admin/pages/MusicSearch.tsx | 11 ++++- src/apps/admin/pages/Overview.tsx | 16 +++++-- src/apps/boards/components/TrackItem.tsx | 2 +- src/components/track-list/SearchTrackItem.tsx | 2 +- src/components/track-list/SearchTrackList.tsx | 2 +- .../track-list/SearchTrackListInfinite.tsx | 18 ++++++++ src/components/track-list/TrackItem.scss | 5 ++- src/components/track-list/TrackItem.tsx | 4 +- src/context/player/PlayerContext.tsx | 1 + src/context/player/utils.ts | 7 ++-- src/types/jukebox.d.ts | 14 +++++++ src/types/spotify.d.ts | 14 +++++++ 14 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 src/components/track-list/SearchTrackListInfinite.tsx diff --git a/src/api/ApiClient.ts b/src/api/ApiClient.ts index 83334a0..b0f028a 100644 --- a/src/api/ApiClient.ts +++ b/src/api/ApiClient.ts @@ -19,6 +19,8 @@ import { mockMemberships } from 'src/utils/mock/mock-memberships' import { MockPlayerState } from '../utils/mock/mock-spotify' import { ApiAuth } from './ApiAuth' +import { parseTrackObj } from 'src/context/player/utils' + /** * Handle API requests to connected servers. */ @@ -276,21 +278,34 @@ export class ApiClient extends ApiAuth { return await this.get(url, { schema: SpotifyAuthRedirectUrlSchema }) } - // TODO: Fix this on backend + /** + * Get trcaks by filters + * @param jukeboxId + * @param trackName + * @param albumName + * @param artistName + * @param pageNum + * @param limit + * @returns + */ public async searchTracks( jukeboxId: number, trackName: string, albumName: string, artistName: string, + pageNum: number, + limit: number, ) { const params = { jukeboxId: jukeboxId, - track: trackName, - album: albumName, - artist: artistName, + trackQuery: trackName, + albumQuery: albumName, + artistQuery: artistName, + page: pageNum, + rows: limit, } const qp = new URLSearchParams() - Object.entries(params).forEach(([k, v]) => qp.append(k, String(v))) + Object.entries(params).filter(([_, v]) => v !== '' && v != null).forEach(([k, v]) => qp.append(k, String(v))) let url = this.endpoints.jukebox.search(jukeboxId) @@ -298,9 +313,22 @@ export class ApiClient extends ApiAuth { console.log(url) - const response = await this.get<{ tracks: ITrack[] }>(url) + const response = await this.get(url) + + if(!response.success){ + return response + } - return response + //Convert the items to ITrack for images + return { + ...response, + data: { + tracks: { + ...response.data.tracks, + items: response.data.tracks?.items.map((track) => parseTrackObj(track)) ?? [] + } + } + } } /** diff --git a/src/apps/admin/pages/JukeSession.tsx b/src/apps/admin/pages/JukeSession.tsx index e9ee6cc..33fc421 100644 --- a/src/apps/admin/pages/JukeSession.tsx +++ b/src/apps/admin/pages/JukeSession.tsx @@ -1,6 +1,6 @@ import { ChangeEvent, useContext, useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { Link, Outlet, useLocation } from 'react-router-dom' +import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom' import { ApiClient } from 'src/api' import { usePopover } from 'src/hooks' import { @@ -31,6 +31,8 @@ export const JukeSession = () => { //Member stuff const [enterCode, setEnterCode] = useState('') + const navigate = useNavigate(); + const handleEnterCode = (e: ChangeEvent) => { const joinCode = e.target.value setEnterCode(joinCode) @@ -51,6 +53,20 @@ export const JukeSession = () => { } } + useEffect(()=>{ + if(jukeSession){ + console.log("active session") + navigate("/dashboard/jam-sessions/active") + } + },[jukeSession]) + + useEffect(()=>{ + if(jukeSession){ + console.log("active session") + navigate("/dashboard/jam-sessions/active") + } + },[]) + const { Popover: JukeSessionPopover, PopoverButton: JukeSessionPopoverButton, diff --git a/src/apps/admin/pages/MusicSearch.tsx b/src/apps/admin/pages/MusicSearch.tsx index b6d38be..3d23d6e 100644 --- a/src/apps/admin/pages/MusicSearch.tsx +++ b/src/apps/admin/pages/MusicSearch.tsx @@ -9,6 +9,10 @@ import './MusicSearch.scss' export const MusicSearch = () => { const [inputs, setInputs] = useState({ track: '', album: '', artist: '' }) const [tracks, setTracks] = useState([]) + const [pagination, setPagination] = useState<{ + page: number + limit: number + }>({ page: 0, limit: 20 }) const jukebox = useSelector(selectCurrentJukebox) const network = ApiClient.getInstance() @@ -31,11 +35,14 @@ export const MusicSearch = () => { inputs.track, inputs.album, inputs.artist, + pagination.page, + pagination.limit, ) console.log(tracksResult) if (tracksResult.success) { - console.log(tracksResult.data.tracks) - setTracks(tracksResult.data.tracks) + console.log(tracksResult.data.tracks.items) + setTracks(tracksResult.data.tracks.items) + //setPagination({page: tracks}) } } else { console.log('Jukebox is not connected') diff --git a/src/apps/admin/pages/Overview.tsx b/src/apps/admin/pages/Overview.tsx index dbd33cc..71bec0a 100644 --- a/src/apps/admin/pages/Overview.tsx +++ b/src/apps/admin/pages/Overview.tsx @@ -3,7 +3,7 @@ import './Overview.scss' import FallbackImg from 'src/assets/img/jukeboxImage.png' import Disk from 'src/assets/svg/Disk.svg?react' -import { useContext } from 'react' +import { useContext, useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { AudioPlayer, TrackList } from 'src/components' import { PlayerContext } from 'src/context' @@ -22,6 +22,16 @@ export const Overview = () => { const adminStatus = useContext(AdminContext) const currentJbxSession = useSelector(selectCurrentJukeSession) + const [trackImg, setTrackImg] = useState(FallbackImg); + + useEffect(()=>{ + if(currentTrack?.image_url){ + setTrackImg(currentTrack?.image_url) + }else{ + setTrackImg(FallbackImg) + } + },[currentTrack?.image_url]) + return ( <> @@ -37,7 +47,7 @@ export const Overview = () => {
{currentTrack?.name} @@ -46,7 +56,7 @@ export const Overview = () => {
{currentTrack?.name} diff --git a/src/apps/boards/components/TrackItem.tsx b/src/apps/boards/components/TrackItem.tsx index a69aa10..1913947 100644 --- a/src/apps/boards/components/TrackItem.tsx +++ b/src/apps/boards/components/TrackItem.tsx @@ -11,7 +11,7 @@ export const TrackItem = (props: { track: ITrack }) => { {track && ( <> - {track.name} + {track.name}

{track.name}

diff --git a/src/components/track-list/SearchTrackItem.tsx b/src/components/track-list/SearchTrackItem.tsx index 507e802..261b3d7 100644 --- a/src/components/track-list/SearchTrackItem.tsx +++ b/src/components/track-list/SearchTrackItem.tsx @@ -39,7 +39,7 @@ export const SearchTrackItem = (props: { track: ITrack }) => { {track && ( <>
- {track.name} + {track.name}

{track.name}

diff --git a/src/components/track-list/SearchTrackList.tsx b/src/components/track-list/SearchTrackList.tsx index 8a7d635..393cb90 100644 --- a/src/components/track-list/SearchTrackList.tsx +++ b/src/components/track-list/SearchTrackList.tsx @@ -10,7 +10,7 @@ export const TrackSearchList = (props: { tracks: ITrack[] }) => { {tracks && tracks.length > 0 && tracks.map( - (track) => track && , + (track) => track && , )} {tracks.length < 1 &&

No tracks available.

} diff --git a/src/components/track-list/SearchTrackListInfinite.tsx b/src/components/track-list/SearchTrackListInfinite.tsx new file mode 100644 index 0000000..b6b17de --- /dev/null +++ b/src/components/track-list/SearchTrackListInfinite.tsx @@ -0,0 +1,18 @@ +import { mergeClassNames } from 'src/utils' + +import { SearchTrackItem } from './SearchTrackItem' + +export const TrackSearchListInfinite = (props: { tracks: ITrack[] }) => { + const { tracks } = props + + return ( +
    + {tracks && + tracks.length > 0 && + tracks.map( + (track) => track && , + )} + {tracks.length < 1 &&

    No tracks available.

    } +
+ ) +} diff --git a/src/components/track-list/TrackItem.scss b/src/components/track-list/TrackItem.scss index bf7ae58..1adb317 100644 --- a/src/components/track-list/TrackItem.scss +++ b/src/components/track-list/TrackItem.scss @@ -9,6 +9,7 @@ padding-bottom: 1.5rem; $flex-g-s: 0 1; + $flex-g-a: 1 1; &::before { content: counter(index, decimal-leading-zero); @@ -24,7 +25,7 @@ } &__preview { - $size: 5rem; + $size: 6rem; display: inline-block; img { @@ -34,7 +35,7 @@ } } &__name-group { - flex: $flex-g-s 45%; + flex: $flex-g-a 45%; } &__name { @include font-title('md'); diff --git a/src/components/track-list/TrackItem.tsx b/src/components/track-list/TrackItem.tsx index 672ae79..7d4e148 100644 --- a/src/components/track-list/TrackItem.tsx +++ b/src/components/track-list/TrackItem.tsx @@ -106,7 +106,7 @@ export const TrackItem = (props: { ) : (
{track.track.name}
@@ -140,7 +140,7 @@ export const TrackItem = (props: { <>
{track.track.name}
diff --git a/src/context/player/PlayerContext.tsx b/src/context/player/PlayerContext.tsx index 69f141a..e51405f 100644 --- a/src/context/player/PlayerContext.tsx +++ b/src/context/player/PlayerContext.tsx @@ -242,6 +242,7 @@ export const PlayerProvider = (props: { children: ReactNode }) => { console.error(res.data) setPlayerError(res.data.message) } else { + console.log("Spootf") setPlayerState(res.data) } } else { diff --git a/src/context/player/utils.ts b/src/context/player/utils.ts index f4365d8..81305ef 100644 --- a/src/context/player/utils.ts +++ b/src/context/player/utils.ts @@ -2,13 +2,14 @@ export const parseTrackObj = (track: Spotify.Track): ITrack => { return { name: track.name, album: track.album.name, - release_year: 0, // TODO: Get from api + release_year: track.album.release_date ? parseInt(track.album.release_date.split('-')[0]): 0, artists: track.artists.map((artist) => artist.name), spotify_id: track.id!, spotify_uri: track.uri, duration_ms: track.duration_ms, - is_explicit: false, // TODO: Get from API - preview_url: null, + is_explicit: track.explicit, + preview_url: null, //depricated + image_url: track.album.images?.[0]?.url ?? null, id: 0, // TODO: Get from API created_at: new Date().toISOString(), updated_at: new Date().toISOString(), diff --git a/src/types/jukebox.d.ts b/src/types/jukebox.d.ts index 58708ac..e70b926 100644 --- a/src/types/jukebox.d.ts +++ b/src/types/jukebox.d.ts @@ -131,6 +131,20 @@ declare interface ITrackCreate { spotify_uri?: string } +interface ITrackSearchResponse { + tracks: IPaginatedSearch +} + +declare interface IPaginatedSearch { + href: string + limit: number + next: string | null + offset: number + previous: string | null + total: number + items: T[] +} + declare interface ITrackUpdate extends Partial {} /* == Player Service == */ diff --git a/src/types/spotify.d.ts b/src/types/spotify.d.ts index 1105e56..7cc11fd 100644 --- a/src/types/spotify.d.ts +++ b/src/types/spotify.d.ts @@ -43,3 +43,17 @@ declare interface ISpotifyPlayerTrack { is_playable: boolean metadata?: any } + +/** + * Extending base Spotify class because + * it excludes needed fields + */ +declare namespace Spotify { + interface Track { + explicit: boolean + } + + interface Album { + release_date: string + } +} \ No newline at end of file From e87b5eff7f7b5c8374fed15a6df73321152b2293 Mon Sep 17 00:00:00 2001 From: Jonathan Hooth Date: Mon, 23 Feb 2026 23:13:29 -0500 Subject: [PATCH 2/4] Minor Fix --- src/apps/admin/components/Topbar.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/apps/admin/components/Topbar.tsx b/src/apps/admin/components/Topbar.tsx index 4cec546..fc8af7b 100644 --- a/src/apps/admin/components/Topbar.tsx +++ b/src/apps/admin/components/Topbar.tsx @@ -61,12 +61,14 @@ export const Topbar = () => { searchInput, '', '', + 0, + 5 ) console.log(tracksResult) if (tracksResult.success) { console.log(tracksResult.data.tracks) //Modify logic for Modal - setSearchList(tracksResult.data.tracks) + setSearchList(tracksResult.data.tracks.items) } } else { console.log('Jukebox is not connected') @@ -98,6 +100,8 @@ export const Topbar = () => { searchInput, '', '', + 0, + 5 ) const query = { From f1897cc04061a6b0bf7c0caadb4610937303c71b Mon Sep 17 00:00:00 2001 From: Jonathan Hooth Date: Thu, 23 Apr 2026 00:55:26 -0400 Subject: [PATCH 3/4] Update for Prod --- src/api/ApiAuth.ts | 2 +- src/api/ApiClient.ts | 38 ++--- .../admin/components/modals/SearchModal.tsx | 9 +- src/components/track-list/SearchTrackItem.tsx | 50 +++++-- .../track-list/TrackInteractions.tsx | 26 ++-- src/components/track-list/TrackItem.tsx | 62 ++------ src/components/track-list/TrackList.tsx | 64 ++++++--- src/context/SocketContext.tsx | 132 +++++++++--------- src/context/player/PlayerContext.tsx | 53 ++++--- src/types/jukebox.d.ts | 2 +- tsconfig.json | 10 +- 11 files changed, 243 insertions(+), 205 deletions(-) diff --git a/src/api/ApiAuth.ts b/src/api/ApiAuth.ts index 3a70613..2ec52f8 100644 --- a/src/api/ApiAuth.ts +++ b/src/api/ApiAuth.ts @@ -131,7 +131,7 @@ export class ApiAuth extends ApiBase { // function getCsrfTokenFromCookie(): string | null { // const cookie = document.cookie - // .split('; ') + // .split('; ') // .find((row) => row.startsWith('csrftoken=')) // return cookie ? decodeURIComponent(cookie.split('=')[1]) : null diff --git a/src/api/ApiClient.ts b/src/api/ApiClient.ts index b0f028a..4380eb2 100644 --- a/src/api/ApiClient.ts +++ b/src/api/ApiClient.ts @@ -280,21 +280,21 @@ export class ApiClient extends ApiAuth { /** * Get trcaks by filters - * @param jukeboxId - * @param trackName - * @param albumName - * @param artistName - * @param pageNum - * @param limit - * @returns + * @param jukeboxId + * @param trackName + * @param albumName + * @param artistName + * @param pageNum + * @param limit + * @returns */ public async searchTracks( jukeboxId: number, trackName: string, albumName: string, artistName: string, - pageNum: number, - limit: number, + pageNum: number = 0, + limit: number = 20, ) { const params = { jukeboxId: jukeboxId, @@ -305,7 +305,9 @@ export class ApiClient extends ApiAuth { rows: limit, } const qp = new URLSearchParams() - Object.entries(params).filter(([_, v]) => v !== '' && v != null).forEach(([k, v]) => qp.append(k, String(v))) + Object.entries(params) + .filter(([_, v]) => v !== '' && v != null) + .forEach(([k, v]) => qp.append(k, String(v))) let url = this.endpoints.jukebox.search(jukeboxId) @@ -314,20 +316,22 @@ export class ApiClient extends ApiAuth { console.log(url) const response = await this.get(url) - - if(!response.success){ + + if (!response.success) { return response } - //Convert the items to ITrack for images + // Convert the items to ITrack for images. return { ...response, data: { tracks: { ...response.data.tracks, - items: response.data.tracks?.items.map((track) => parseTrackObj(track)) ?? [] - } - } + items: + response.data.tracks?.items.map((track) => parseTrackObj(track)) ?? + [], + }, + }, } } @@ -372,7 +376,7 @@ export class ApiClient extends ApiAuth { body: ISetQueueOrder, ) { const url = this.endpoints.jukebox.queueTrackList(jukeboxId, jukeSessionId) - return await this.post(url, { + return await this.put(url, { body, }) } diff --git a/src/apps/admin/components/modals/SearchModal.tsx b/src/apps/admin/components/modals/SearchModal.tsx index 8e01b71..08f489c 100644 --- a/src/apps/admin/components/modals/SearchModal.tsx +++ b/src/apps/admin/components/modals/SearchModal.tsx @@ -47,12 +47,17 @@ export const SearchModal = ({ changeState(false) } + const previewTracks = tracks.slice(0, 4) + return ( <>
- {tracks.splice(0, 4).map((track, key) => ( -
+ {previewTracks.map((track) => ( +
{track.name}
diff --git a/src/components/track-list/SearchTrackItem.tsx b/src/components/track-list/SearchTrackItem.tsx index 261b3d7..34cb61e 100644 --- a/src/components/track-list/SearchTrackItem.tsx +++ b/src/components/track-list/SearchTrackItem.tsx @@ -1,11 +1,12 @@ import { useState } from 'react' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { ApiClient } from 'src/api' import { selectCurrentJukebox, selectCurrentJukeSession, - selectUser, + selectCurrentJukeSessionMembership, } from 'src/store' +import { thunkFetchQueue } from 'src/store/jukebox/jbxThunks' import { formatDuration } from 'src/utils' import './SearchTrackItem.scss' @@ -14,22 +15,46 @@ export const SearchTrackItem = (props: { track: ITrack }) => { const { track } = props const network = ApiClient.getInstance() + const dispatch = useDispatch() const jukebox = useSelector(selectCurrentJukebox) const jukeSession = useSelector(selectCurrentJukeSession) - const user = useSelector(selectUser) + const jukeSessionMembership = useSelector(selectCurrentJukeSessionMembership) const [addedToQueue, setAddedToQueue] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) const addSongToQueue = async () => { - setAddedToQueue(true) - if (track && jukebox && jukeSession) { + if ( + !track || + !jukebox || + !jukeSession || + !jukeSessionMembership || + isSubmitting + ) { + return + } + + setIsSubmitting(true) + + try { const res = await network.queueTrack(jukebox.id, jukeSession.id, { spotify_track_id: track.spotify_id, - queued_by: { id: user!.id }, + queued_by: { id: jukeSessionMembership.id }, }) - console.log(res) - } else { - console.log('Not Possible') + + if (!res.success) { + return + } + + setAddedToQueue(true) + await dispatch( + thunkFetchQueue({ + jukeboxId: jukebox.id, + jukeSessionId: jukeSession.id, + }) as any, + ) + } finally { + setIsSubmitting(false) } } @@ -54,7 +79,12 @@ export const SearchTrackItem = (props: { track: ITrack }) => {
Added
) : (
-
diff --git a/src/components/track-list/TrackInteractions.tsx b/src/components/track-list/TrackInteractions.tsx index 3271132..8335dbc 100644 --- a/src/components/track-list/TrackInteractions.tsx +++ b/src/components/track-list/TrackInteractions.tsx @@ -1,14 +1,15 @@ import { ThumbDownAltOutlined, ThumbUpAltOutlined } from '@mui/icons-material' -import { useContext, useEffect } from 'react' +import { useContext } from 'react' import './TrackInteractions.scss' import { useDrag } from 'react-dnd' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { ApiClient } from 'src/api' import { AdminContext } from 'src/apps/admin' import { TrackModifyContext } from 'src/apps/admin/pages/trackContext' import { MoveIcon, RemoveIcon } from 'src/assets/Icons' -import { selectCurrentJukeSession, selectCurrentMembership } from 'src/store' +import { selectCurrentJukeSession } from 'src/store' +import { thunkFetchQueue } from 'src/store/jukebox/jbxThunks' export const TrackInteractions = (props: { track: IQueuedTrack @@ -19,17 +20,26 @@ export const TrackInteractions = (props: { const adminStatus = useContext(AdminContext) const trackStatus = useContext(TrackModifyContext) const network = ApiClient.getInstance() + const dispatch = useDispatch() const jukeSession = useSelector(selectCurrentJukeSession) - const currentMembership = useSelector(selectCurrentMembership) - const removeTrack = () => { + const removeTrack = async () => { if (track.id != null && adminStatus.jukebox !== null && jukeSession) { - network.removeQueuedTrack( + const res = await network.removeQueuedTrack( adminStatus.jukebox.id, jukeSession.id, track.id, ) + + if (res.success) { + await dispatch( + thunkFetchQueue({ + jukeboxId: adminStatus.jukebox.id, + jukeSessionId: jukeSession.id, + }) as any, + ) + } } } @@ -41,10 +51,6 @@ export const TrackInteractions = (props: { }), }) - useEffect(() => { - console.log(adminStatus.role) - }, [currentMembership]) - return ( <> {adminStatus.role === 'admin' && trackStatus ? ( diff --git a/src/components/track-list/TrackItem.tsx b/src/components/track-list/TrackItem.tsx index 7d4e148..dc9a3de 100644 --- a/src/components/track-list/TrackItem.tsx +++ b/src/components/track-list/TrackItem.tsx @@ -1,35 +1,33 @@ import { formatDuration } from 'src/utils' -import { useContext, useEffect, useRef, useState } from 'react' +import { useContext, useRef } from 'react' import { useDrop } from 'react-dnd' -import { useSelector } from 'react-redux' -import { ApiClient } from 'src/api' import { AdminContext } from 'src/apps/admin' -import { selectCurrentJukebox } from 'src/store' import { TrackInteractions } from './TrackInteractions' import './TrackItem.scss' export const TrackItem = (props: { track: Nullable moveListItem: (dragIndex: number, hoverIndex: number) => void + persistQueueOrder: () => Promise index: number showIcon: boolean showLength: boolean }) => { - const { track, moveListItem, index, showIcon, showLength } = props + const { + track, + moveListItem, + persistQueueOrder, + index, + showIcon, + showLength, + } = props const adminStatus = useContext(AdminContext) const ref = useRef(null) - const network = ApiClient.getInstance() - const currentJukebox = useSelector(selectCurrentJukebox) - - const [targetPos, setTargetPos] = useState(-1) - const [hoverIndexNum, setHoverIndexNum] = useState(-1) - const [originalIndex, setOriginalIndex] = useState(index) - - const [spec, dropRef] = useDrop({ + const [, dropRef] = useDrop({ accept: 'track', hover: (item: any, monitor: any) => { const dragIndex = item.index @@ -51,49 +49,15 @@ export const TrackItem = (props: { // if dragging up, continue only when hover is bigger than middle Y if (dragIndex > hoverIndex && hoverActualY > hoverMiddleY) return - props.moveListItem(dragIndex, hoverIndex) moveListItem(dragIndex, hoverIndex) - setTargetPos(dragIndex) - item.index = hoverIndex }, - drop: (item: any, monitor: any) => { - console.log(`Moving the ${originalIndex} to ${targetPos}`) - if (currentJukebox) { - // TODO: Fix swapping tracks - // const res = network.swapTracks( - // currentJukebox.id, - // originalIndex, - // targetPos, - // ) - // console.log(res) - } - - //update track position - setOriginalIndex(targetPos) + drop: () => { + void persistQueueOrder() }, }) - - /* - const [{ isDragging }, dragRef] = useDrag({ - type: 'track', - item: { index }, - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), - }) - - const draggingStyle: React.CSSProperties = isDragging ? { - border: '2px solid red'} : {} - */ - const dropperRef = dropRef(ref) dropRef(ref) - //I think it works unintentionally, assuming the queue id changes - useEffect(() => { - setOriginalIndex(index) - }, [track]) - return ( <> {adminStatus.role === 'admin' ? ( diff --git a/src/components/track-list/TrackList.tsx b/src/components/track-list/TrackList.tsx index 74acf2b..63159b0 100644 --- a/src/components/track-list/TrackList.tsx +++ b/src/components/track-list/TrackList.tsx @@ -1,4 +1,8 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { ApiClient } from 'src/api' +import { selectCurrentJukebox, selectCurrentJukeSession } from 'src/store' +import { thunkFetchQueue } from 'src/store/jukebox/jbxThunks' import { mergeClassNames } from 'src/utils' import { TrackItem } from './TrackItem' import './TrackList.scss' @@ -11,25 +15,48 @@ export const TrackList = (props: { showLength?: boolean }) => { const { tracks, offsetCount, maxCount, showIcon, showLength } = props + const api = ApiClient.getInstance() + const dispatch = useDispatch() + const jukebox = useSelector(selectCurrentJukebox) + const jukeSession = useSelector(selectCurrentJukeSession) const initialCopy = useMemo(() => structuredClone(tracks), [tracks]) const [queuedTracks, swapTracks] = useState(initialCopy) + const queuedTracksRef = useRef(initialCopy) - const moveListItem = useCallback( - (dragIndex: number, hoverIndex: number) => { - const dragItem = queuedTracks[dragIndex] - const hoverItem = queuedTracks[hoverIndex] - //Swap places of Items - swapTracks((queuedTracks: any) => { - const updatedTracks = [...queuedTracks] - updatedTracks[dragIndex] = hoverItem - updatedTracks[hoverIndex] = dragItem - return updatedTracks - }) - }, - [queuedTracks], - ) + useEffect(() => { + queuedTracksRef.current = queuedTracks + }, [queuedTracks]) + + const moveListItem = useCallback((dragIndex: number, hoverIndex: number) => { + swapTracks((currentTracks) => { + const updatedTracks = [...currentTracks] + const [dragItem] = updatedTracks.splice(dragIndex, 1) + updatedTracks.splice(hoverIndex, 0, dragItem) + return updatedTracks + }) + }, []) + + const persistQueueOrder = useCallback(async () => { + if (!jukebox || !jukeSession) { + return + } + + const ordering = queuedTracksRef.current.map((track) => track.id) + const res = await api.setQueueOrder(jukebox.id, jukeSession.id, { + ordering, + }) + + if (res.success) { + await dispatch( + thunkFetchQueue({ + jukeboxId: jukebox.id, + jukeSessionId: jukeSession.id, + }) as any, + ) + } + }, [api, dispatch, jukebox, jukeSession]) useEffect(() => { swapTracks(structuredClone(tracks)) @@ -52,13 +79,14 @@ export const TrackList = (props: { track={track} key={track.id} moveListItem={moveListItem} + persistQueueOrder={persistQueueOrder} index={index} - showIcon={showIcon !== undefined ? false : true} - showLength={showLength !== undefined ? false : true} + showIcon={showIcon ?? true} + showLength={showLength ?? true} /> ), ) - .splice(0, maxCount ?? tracks.length)} + .slice(0, maxCount ?? tracks.length)} {tracks.length < 1 &&

No tracks available.

} ) diff --git a/src/context/SocketContext.tsx b/src/context/SocketContext.tsx index 621aa3f..3ab8c4f 100644 --- a/src/context/SocketContext.tsx +++ b/src/context/SocketContext.tsx @@ -12,116 +12,110 @@ import { export const SocketContext = createContext({ emitMessage: (ev: string, message: T) => {}, - onEvent: (ev: string, cb: (message: T) => void) => {}, + onEvent: + (ev: string, cb: (message: T) => void) => + () => {}, + isConnected: false, socket: {} as MutableRefObject | null>, }) export const SocketProvider = (props: { children: ReactNode }) => { const socket = useRef(null) const isLoggedIn = useSelector(selectUserLoggedIn) - const [subEvents, setSubEvents] = useState([]) - const [callbacks, setCallbacks] = useState>({}) const jukebox = useSelector(selectCurrentJukebox) const membership = useSelector(selectCurrentMembership) const token = ApiClient.getInstance().token - const [socketLoggedIn, setSocketLoggedIn] = useState(false) + const [isConnected, setIsConnected] = useState(false) useEffect(() => { - if (isLoggedIn && jukebox) { - console.log('socket token:', token) - const role = membership?.is_admin ? 'admin' : 'member' - const clubId = jukebox.club_id - socket.current = connect(`${SOCKET_URL}`, { - auth: { - token: token, - }, - query: { - club_id: clubId, - role: role, - }, - }) - setSocketLoggedIn(true) + if ( + REACT_ENV === 'dev' || + !isLoggedIn || + !jukebox || + !membership || + !token + ) { + socket.current?.disconnect() + socket.current = null + setIsConnected(false) + return } - }, [isLoggedIn, jukebox, membership]) - useEffect(() => { - if (socket.current) { - socket.current.emit('subscribe', {}) - } - }, [socket]) + const role = membership.is_admin ? 'admin' : 'member' + const nextSocket = connect(`${SOCKET_URL}`, { + auth: { + token, + }, + query: { + club_id: jukebox.club_id, + role, + }, + }) - useEffect(() => { - if (REACT_ENV === 'dev' || !socket.current) return + socket.current = nextSocket const onConnect = () => { console.log('Socket connected.') + setIsConnected(true) } const onDisconnect = () => { console.log('Socket disconnected.') + setIsConnected(false) } - socket.current.on('connect', onConnect) - socket.current.on('disconnect', onDisconnect) - socket.current.on('exception', (err) => { + const onException = (err: { message?: string }) => { console.error(`Socket connection error due to ${err.message}`) - }) - socket.current.on('ping-pong', (data: string) => { - window.alert(`Pong: ${data}`) - }) + } - socket.current.onAny((event, ...args) => { + nextSocket.on('connect', onConnect) + nextSocket.on('disconnect', onDisconnect) + nextSocket.on('exception', onException) + nextSocket.onAny((event, ...args) => { console.debug('Socket received:', event, args) }) return () => { - if (socket.current) { - socket.current.off('connect', onConnect) - socket.current.off('disconnect', onDisconnect) - socket.current.off() + nextSocket.off('connect', onConnect) + nextSocket.off('disconnect', onDisconnect) + nextSocket.off('exception', onException) + nextSocket.offAny() + nextSocket.disconnect() + if (socket.current === nextSocket) { + socket.current = null } + setIsConnected(false) } - }, []) + }, [isLoggedIn, jukebox?.club_id, membership?.is_admin, token]) - const emitMessage = (ev: string, message: any) => { + const emitMessage = useCallback((ev: string, message: any) => { if (REACT_ENV === 'dev' || !socket.current) return console.debug(`Emitting message for ${ev}:`, message) socket.current.emit(ev, message) - } + }, []) - const onEvent = useCallback( - (ev: string, cb: (message: T) => void) => { - if (REACT_ENV === 'dev') return + const onEvent = useCallback((ev: string, cb: (message: T) => void) => { + if (REACT_ENV === 'dev' || !socket.current) { + return () => {} + } - const wrappedCb = (message: T) => { - console.debug(`Receiving message for ${ev}:`, message) - return cb(message) - } - // socket.current.on(ev, wrappedCb) - setCallbacks((prev) => ({ ...prev, [ev]: wrappedCb })) - }, - [setCallbacks], - ) + const activeSocket = socket.current + const wrappedCb = (message: T) => { + console.debug(`Receiving message for ${ev}:`, message) + cb(message) + } - useEffect(() => { - //console.log('current:', socket.current) - //console.log('logged in:', socketLoggedIn) - if (!socket.current || !socketLoggedIn) return - //console.log('callbacks:', callbacks) - - for (const [ev, cb] of Object.entries(callbacks)) { - //console.log('registering event:', ev) - socket.current.on(ev, cb) + activeSocket.on(ev, wrappedCb) + + return () => { + activeSocket.off(ev, wrappedCb) } - }, [ - Object.keys(callbacks), - socket.current, - socket.current?.active, - socketLoggedIn, - ]) + }, []) return ( - + {props.children} ) diff --git a/src/context/player/PlayerContext.tsx b/src/context/player/PlayerContext.tsx index e51405f..df235c2 100644 --- a/src/context/player/PlayerContext.tsx +++ b/src/context/player/PlayerContext.tsx @@ -67,7 +67,7 @@ export const PlayerProvider = (props: { children: ReactNode }) => { hasAuxRef.current = hasAux }, [hasAux]) - const { onEvent, emitMessage, socket } = useContext(SocketContext) + const { onEvent, emitMessage, isConnected } = useContext(SocketContext) // =============================================================== // Track State Sync @@ -91,9 +91,24 @@ export const PlayerProvider = (props: { children: ReactNode }) => { }, []) useEffect(() => { - onEvent('player-join-success', updateTrackStateFromSocket) - onEvent('player-state-update', updateTrackStateFromSocket) - }, [updateTrackStateFromSocket]) + if (!isConnected) { + return + } + + const offJoinSuccess = onEvent( + 'player-join-success', + updateTrackStateFromSocket, + ) + const offPlayerState = onEvent( + 'player-state-update', + updateTrackStateFromSocket, + ) + + return () => { + offJoinSuccess() + offPlayerState() + } + }, [isConnected, onEvent, updateTrackStateFromSocket]) // When player state changes, sync current track useEffect(() => { @@ -106,7 +121,7 @@ export const PlayerProvider = (props: { children: ReactNode }) => { // Tick live progress forward every second while playing useEffect(() => { - if (!playerState?.progress) { + if (!playerState || playerState.progress == null) { setLiveProgress(0) return } @@ -143,22 +158,14 @@ export const PlayerProvider = (props: { children: ReactNode }) => { // When jukebox changes and user doesn't have aux, join for socket updates useEffect(() => { - //console.log('[PlayerContext] Join check:', { - // jukeboxId: jukebox?.id, - // hasAux, - // socketConnected: socket?.current?.connected, - // socketId: socket?.current?.id, - // willJoin: !!(jukebox?.id && !hasAux) - //}) - - if (!jukebox?.id || hasAux || !socket?.current?.connected) { + if (!jukebox?.id || hasAux || !isConnected) { return } emitMessage<{ jukebox_id: number }>('player-join', { jukebox_id: jukebox.id, }) - }, [jukebox?.id, hasAux, socket, emitMessage]) + }, [jukebox?.id, hasAux, isConnected, emitMessage]) // =============================================================== // Player Aux Updates @@ -201,7 +208,7 @@ export const PlayerProvider = (props: { children: ReactNode }) => { action: playerState.is_playing ? 'played' : 'paused', spotify_track: playerState.spotify_track, }) - }, [hasAux, playerState?.is_playing]) + }, [hasAux, jukebox?.id, playerState?.is_playing, playerState?.spotify_track]) // Emit changed_tracks only when progress actually hits 0 (track flip) const progressIsZero = playerState?.progress === 0 @@ -216,12 +223,19 @@ export const PlayerProvider = (props: { children: ReactNode }) => { timestamp: new Date(), duration_ms: playerState.spotify_track?.duration_ms, }) - }, [hasAux, progressIsZero]) + }, [ + hasAux, + jukebox?.id, + progressIsZero, + playerState, + playerState?.spotify_track, + ]) // Emit progress — but only on actual Spotify state updates, not the local // timer ticks. We use playerState.progress (set from auxPlayerState) not liveProgress. useEffect(() => { - if (!jukebox || !hasAux || !playerState?.progress) return + if (!jukebox || !hasAux || !playerState || playerState.progress == null) + return emitMessage('player-aux-update', { jukebox_id: jukebox.id, @@ -229,7 +243,7 @@ export const PlayerProvider = (props: { children: ReactNode }) => { progress: playerState.progress, spotify_track: playerState.spotify_track, //the slop maker failed me }) - }, [hasAux, playerState?.progress]) + }, [hasAux, jukebox?.id, playerState?.progress, playerState?.spotify_track]) // =============================================================== // Connect Device @@ -242,7 +256,6 @@ export const PlayerProvider = (props: { children: ReactNode }) => { console.error(res.data) setPlayerError(res.data.message) } else { - console.log("Spootf") setPlayerState(res.data) } } else { diff --git a/src/types/jukebox.d.ts b/src/types/jukebox.d.ts index e70b926..2ea4c19 100644 --- a/src/types/jukebox.d.ts +++ b/src/types/jukebox.d.ts @@ -48,7 +48,7 @@ declare interface IJukeSessionUpdate { is_active?: boolean } -declare interface IJukeSessionMembership { +declare interface IJukeSessionMembership extends IModelBase { juke_session: number user_id: number queued_tracks: number[] diff --git a/tsconfig.json b/tsconfig.json index 2bf1f5b..f8941a2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,13 +37,7 @@ "src/*": ["./src/*"] } }, - "include": [ - "src", - "./src/*", - "docs", - "src/_deprecated/_oldSpotifyPlayer.tsx", - "src/_deprecated/old_spotify_old.ts" - ], + "include": ["src", "./src/*", "docs"], "references": [{ "path": "./tsconfig.node.json" }], - "exclude": ["node_modules/", ".prettierrc.cjs"] + "exclude": ["node_modules/", ".prettierrc.cjs", "src/_deprecated"] } From 12924c70ad74ef00d68d750c106585a2e5af18ec Mon Sep 17 00:00:00 2001 From: Jonathan Hooth Date: Thu, 23 Apr 2026 01:00:05 -0400 Subject: [PATCH 4/4] fixed lint --- src/api/ApiAuth.ts | 2 +- src/apps/admin/components/Topbar.tsx | 4 +- src/apps/admin/pages/JukeSession.tsx | 22 +++++------ src/apps/admin/pages/Overview.tsx | 10 ++--- src/components/track-list/SearchTrackList.tsx | 3 +- .../track-list/SearchTrackListInfinite.tsx | 37 ++++++++++--------- src/context/player/utils.ts | 4 +- src/types/spotify.d.ts | 2 +- 8 files changed, 44 insertions(+), 40 deletions(-) diff --git a/src/api/ApiAuth.ts b/src/api/ApiAuth.ts index 2ec52f8..3a70613 100644 --- a/src/api/ApiAuth.ts +++ b/src/api/ApiAuth.ts @@ -131,7 +131,7 @@ export class ApiAuth extends ApiBase { // function getCsrfTokenFromCookie(): string | null { // const cookie = document.cookie - // .split('; ') + // .split('; ') // .find((row) => row.startsWith('csrftoken=')) // return cookie ? decodeURIComponent(cookie.split('=')[1]) : null diff --git a/src/apps/admin/components/Topbar.tsx b/src/apps/admin/components/Topbar.tsx index fc8af7b..fe6ed79 100644 --- a/src/apps/admin/components/Topbar.tsx +++ b/src/apps/admin/components/Topbar.tsx @@ -62,7 +62,7 @@ export const Topbar = () => { '', '', 0, - 5 + 5, ) console.log(tracksResult) if (tracksResult.success) { @@ -101,7 +101,7 @@ export const Topbar = () => { '', '', 0, - 5 + 5, ) const query = { diff --git a/src/apps/admin/pages/JukeSession.tsx b/src/apps/admin/pages/JukeSession.tsx index 33fc421..2628649 100644 --- a/src/apps/admin/pages/JukeSession.tsx +++ b/src/apps/admin/pages/JukeSession.tsx @@ -31,7 +31,7 @@ export const JukeSession = () => { //Member stuff const [enterCode, setEnterCode] = useState('') - const navigate = useNavigate(); + const navigate = useNavigate() const handleEnterCode = (e: ChangeEvent) => { const joinCode = e.target.value @@ -53,19 +53,19 @@ export const JukeSession = () => { } } - useEffect(()=>{ - if(jukeSession){ - console.log("active session") - navigate("/dashboard/jam-sessions/active") + useEffect(() => { + if (jukeSession) { + console.log('active session') + navigate('/dashboard/jam-sessions/active') } - },[jukeSession]) + }, [jukeSession]) - useEffect(()=>{ - if(jukeSession){ - console.log("active session") - navigate("/dashboard/jam-sessions/active") + useEffect(() => { + if (jukeSession) { + console.log('active session') + navigate('/dashboard/jam-sessions/active') } - },[]) + }, []) const { Popover: JukeSessionPopover, diff --git a/src/apps/admin/pages/Overview.tsx b/src/apps/admin/pages/Overview.tsx index 71bec0a..23a68d6 100644 --- a/src/apps/admin/pages/Overview.tsx +++ b/src/apps/admin/pages/Overview.tsx @@ -22,15 +22,15 @@ export const Overview = () => { const adminStatus = useContext(AdminContext) const currentJbxSession = useSelector(selectCurrentJukeSession) - const [trackImg, setTrackImg] = useState(FallbackImg); + const [trackImg, setTrackImg] = useState(FallbackImg) - useEffect(()=>{ - if(currentTrack?.image_url){ + useEffect(() => { + if (currentTrack?.image_url) { setTrackImg(currentTrack?.image_url) - }else{ + } else { setTrackImg(FallbackImg) } - },[currentTrack?.image_url]) + }, [currentTrack?.image_url]) return ( <> diff --git a/src/components/track-list/SearchTrackList.tsx b/src/components/track-list/SearchTrackList.tsx index 393cb90..5540fd9 100644 --- a/src/components/track-list/SearchTrackList.tsx +++ b/src/components/track-list/SearchTrackList.tsx @@ -10,7 +10,8 @@ export const TrackSearchList = (props: { tracks: ITrack[] }) => { {tracks && tracks.length > 0 && tracks.map( - (track) => track && , + (track) => + track && , )} {tracks.length < 1 &&

No tracks available.

} diff --git a/src/components/track-list/SearchTrackListInfinite.tsx b/src/components/track-list/SearchTrackListInfinite.tsx index b6b17de..a8e4929 100644 --- a/src/components/track-list/SearchTrackListInfinite.tsx +++ b/src/components/track-list/SearchTrackListInfinite.tsx @@ -1,18 +1,19 @@ -import { mergeClassNames } from 'src/utils' - -import { SearchTrackItem } from './SearchTrackItem' - -export const TrackSearchListInfinite = (props: { tracks: ITrack[] }) => { - const { tracks } = props - - return ( -
    - {tracks && - tracks.length > 0 && - tracks.map( - (track) => track && , - )} - {tracks.length < 1 &&

    No tracks available.

    } -
- ) -} +import { mergeClassNames } from 'src/utils' + +import { SearchTrackItem } from './SearchTrackItem' + +export const TrackSearchListInfinite = (props: { tracks: ITrack[] }) => { + const { tracks } = props + + return ( +
    + {tracks && + tracks.length > 0 && + tracks.map( + (track) => + track && , + )} + {tracks.length < 1 &&

    No tracks available.

    } +
+ ) +} diff --git a/src/context/player/utils.ts b/src/context/player/utils.ts index 81305ef..473eaf8 100644 --- a/src/context/player/utils.ts +++ b/src/context/player/utils.ts @@ -2,7 +2,9 @@ export const parseTrackObj = (track: Spotify.Track): ITrack => { return { name: track.name, album: track.album.name, - release_year: track.album.release_date ? parseInt(track.album.release_date.split('-')[0]): 0, + release_year: track.album.release_date + ? parseInt(track.album.release_date.split('-')[0]) + : 0, artists: track.artists.map((artist) => artist.name), spotify_id: track.id!, spotify_uri: track.uri, diff --git a/src/types/spotify.d.ts b/src/types/spotify.d.ts index 7cc11fd..6136607 100644 --- a/src/types/spotify.d.ts +++ b/src/types/spotify.d.ts @@ -56,4 +56,4 @@ declare namespace Spotify { interface Album { release_date: string } -} \ No newline at end of file +}