From 3f70aed96a215242eccef6d068862604a043bae1 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 18 Jul 2025 09:25:55 -0500 Subject: [PATCH] Signed-off-by: Matt Bruce --- src/App.css | 34 ++ src/components/common/InfiniteScrollList.tsx | 2 +- src/features/Artists/Artists.tsx | 13 +- src/features/Favorites/Favorites.tsx | 12 - src/features/History/History.tsx | 13 +- src/features/NewSongs/NewSongs.tsx | 12 - src/features/Queue/Queue.tsx | 206 +++++------ src/features/Search/Search.tsx | 8 +- src/features/SongLists/SongLists.tsx | 351 ++++++++----------- src/features/TopPlayed/Top100.tsx | 12 - 10 files changed, 272 insertions(+), 391 deletions(-) diff --git a/src/App.css b/src/App.css index b9d355d..60fdab0 100644 --- a/src/App.css +++ b/src/App.css @@ -40,3 +40,37 @@ .read-the-docs { color: #888; } + +/* Override Ionic accordion separator styling with higher specificity */ +ion-accordion.accordion-no-full-separator { + --border-style: none !important; +} + +ion-accordion.accordion-no-full-separator::part(header) { + --border-style: none !important; +} + +ion-accordion.accordion-no-full-separator ion-item.item-no-full-separator { + --border-style: solid !important; + --border-width: 0 0 1px 0 !important; + --border-color: var(--ion-item-border-color, rgba(0, 0, 0, 0.13)) !important; +} + +ion-accordion.accordion-no-full-separator ion-item.item-no-full-separator::part(native) { + --border-style: solid !important; + --border-width: 0 0 1px 0 !important; + --border-color: var(--ion-item-border-color, rgba(0, 0, 0, 0.13)) !important; +} + +/* Alternative approach: Target all accordion items globally */ +ion-accordion ion-item { + --border-style: solid !important; + --border-width: 0 0 1px 0 !important; + --border-color: var(--ion-item-border-color, rgba(0, 0, 0, 0.13)) !important; +} + +ion-accordion ion-item::part(native) { + --border-style: solid !important; + --border-width: 0 0 1px 0 !important; + --border-color: var(--ion-item-border-color, rgba(0, 0, 0, 0.13)) !important; +} diff --git a/src/components/common/InfiniteScrollList.tsx b/src/components/common/InfiniteScrollList.tsx index 2da9c22..0ab6da9 100644 --- a/src/components/common/InfiniteScrollList.tsx +++ b/src/components/common/InfiniteScrollList.tsx @@ -114,7 +114,7 @@ const InfiniteScrollList = ({ {/* Stats */} {items.length > 0 && ( -
+
Showing {items.length} item{items.length !== 1 ? 's' : ''} {hasMore && ` • Scroll down to load more`}
diff --git a/src/features/Artists/Artists.tsx b/src/features/Artists/Artists.tsx index db2de08..de8bf47 100644 --- a/src/features/Artists/Artists.tsx +++ b/src/features/Artists/Artists.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon } from '@ionic/react'; +import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent } from '@ionic/react'; import { close, add, heart, heartOutline, list } from 'ionicons/icons'; import { InfiniteScrollList, PageHeader } from '../../components/common'; import { useArtists } from '../../hooks'; @@ -56,15 +56,8 @@ const Artists: React.FC = () => { return ( <> - - - Artists - - -
@@ -103,7 +96,7 @@ const Artists: React.FC = () => { -
+ {selectedArtistSongs.map((song) => ( @@ -134,7 +127,7 @@ const Artists: React.FC = () => { ))} -
+
diff --git a/src/features/Favorites/Favorites.tsx b/src/features/Favorites/Favorites.tsx index 9e91ea9..9d9de52 100644 --- a/src/features/Favorites/Favorites.tsx +++ b/src/features/Favorites/Favorites.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react'; import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common'; import { useFavorites } from '../../hooks'; import { useAppSelector } from '../../redux'; @@ -24,17 +23,6 @@ const Favorites: React.FC = () => { return ( <> - - - - Favorites - - {favoritesItems.length} - - - - - { return ( <> - - - - Recently Played - - {historyItems.length} - - - - - { return ( <> - - - - New Songs - - {newSongsItems.length} - - - - - { const { @@ -32,124 +33,101 @@ const Queue: React.FC = () => { console.log('Queue component - canDeleteFirstItem:', canDeleteFirstItem); console.log('Queue component - canReorder:', canReorder); - return ( -
-
-

Queue

-

- {queueStats.totalSongs} song{queueStats.totalSongs !== 1 ? 's' : ''} in queue -

- - {/* Debug info */} -
- Queue items loaded: {queueCount} -
-
+ // Render queue item for InfiniteScrollList + const renderQueueItem = (queueItem: QueueItem, index: number) => { + console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`); + const canDelete = index === 0 ? canDeleteFirstItem : true; + + return ( + + + {/* Order Number */} +
+ {queueItem.order} +
- {/* Player Controls - Only visible to admin users */} -
- -
+ {/* Song Info */} + +

+ {queueItem.song.title} +

+

+ {queueItem.song.artist} +

+
- {/* Queue List */} -
- {queueCount === 0 ? ( - - - - } - /> - ) : queueItems.length === 0 ? ( - - - - } - /> - ) : ( - - {queueItems.map((queueItem, index) => { - console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`); - const canDelete = index === 0 ? canDeleteFirstItem : true; - - return ( - - - {/* Order Number */} -
- {queueItem.order} -
+ {/* Singer Pill - Moved to far right */} + + {queueItem.singer.name} + - {/* Song Info */} - -

- {queueItem.song.title} -

-

- {queueItem.song.artist} -

-
- - {queueItem.singer.name} - - {queueItem.isCurrentUser && ( - - You - - )} -
-
+ {/* Admin Controls */} + {canReorder && ( +
+ {queueItem.order > 2 && ( + handleMoveUp(queueItem)} + variant="secondary" + size="sm" + > + + + )} + {queueItem.order > 1 && queueItem.order < queueItems.length && ( + handleMoveDown(queueItem)} + variant="secondary" + size="sm" + > + + + )} +
+ )} +
- {/* Admin Controls */} - {canReorder && ( -
- {queueItem.order > 2 && ( - handleMoveUp(queueItem)} - variant="secondary" - size="sm" - > - - - )} - {queueItem.order > 1 && queueItem.order < queueItems.length && ( - handleMoveDown(queueItem)} - variant="secondary" - size="sm" - > - - - )} -
- )} - - - {/* Swipe Actions */} - {canDelete && ( - - handleRemoveFromQueue(queueItem)} - > - - - - )} -
- ); - })} -
+ {/* Swipe Actions */} + {canDelete && ( + + handleRemoveFromQueue(queueItem)} + > + + + )} + + ); + }; + + return ( + <> + + +
+ {/* Player Controls - Only visible to admin users */} +
+ +
+ + {/* Queue List */} + + items={queueItems} + isLoading={queueCount === 0} + hasMore={false} + onLoadMore={() => {}} + renderItem={renderQueueItem} + emptyTitle="Queue is empty" + emptyMessage="Add songs from search, history, or favorites to get started" + loadingTitle="Loading queue..." + loadingMessage="Please wait while queue data is being loaded" + />
-
+ ); }; diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index f9ebf14..2fd2672 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -34,12 +34,13 @@ const Search: React.FC = () => { // Debug logging console.log('Search component - songs count:', songsCount); console.log('Search component - search results:', searchResults); + console.log('Search component - search term:', searchTerm); + console.log('Search component - showing:', searchResults.songs.length, 'of', searchResults.count); return (
@@ -51,11 +52,6 @@ const Search: React.FC = () => { debounce={300} showClearButton="focus" /> - - {/* Debug info */} -
- Total songs loaded: {songsCount} | Showing: {searchResults.songs.length} of {searchResults.count} | Page: {searchResults.currentPage}/{searchResults.totalPages} | Search term: "{searchTerm}" -
{/* Search Results */} diff --git a/src/features/SongLists/SongLists.tsx b/src/features/SongLists/SongLists.tsx index a7311cd..2df501a 100644 --- a/src/features/SongLists/SongLists.tsx +++ b/src/features/SongLists/SongLists.tsx @@ -1,10 +1,11 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { IonHeader, IonToolbar, IonTitle, IonList, IonItem, IonLabel, IonModal, IonButton, IonIcon, IonChip, IonAccordion, IonAccordionGroup } from '@ionic/react'; -import { close, documentText, add, heart, heartOutline } from 'ionicons/icons'; +import React, { useState } from 'react'; +import { IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonChip, IonContent, IonList, IonAccordionGroup, IonAccordion } from '@ionic/react'; +import { close, list } from 'ionicons/icons'; +import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common'; import { useSongLists } from '../../hooks'; import { useAppSelector } from '../../redux'; import { selectSongList } from '../../redux'; -import type { SongListSong, Song } from '../../types'; +import type { SongListSong, SongList, Song } from '../../types'; const SongLists: React.FC = () => { const { @@ -19,237 +20,163 @@ const SongLists: React.FC = () => { const songListData = useAppSelector(selectSongList); const songListCount = Object.keys(songListData).length; - const observerRef = useRef(null); - - // Intersection Observer for infinite scrolling - useEffect(() => { - console.log('SongLists - Setting up observer:', { hasMore, songListCount, itemsLength: songLists.length }); - - const observer = new IntersectionObserver( - (entries) => { - console.log('SongLists - Intersection detected:', { - isIntersecting: entries[0].isIntersecting, - hasMore, - songListCount - }); - - if (entries[0].isIntersecting && hasMore && songListCount > 0) { - console.log('SongLists - Loading more items'); - loadMore(); - } - }, - { threshold: 0.1 } - ); - - if (observerRef.current) { - observer.observe(observerRef.current); - } - - return () => observer.disconnect(); - }, [loadMore, hasMore, songListCount]); const [selectedSongList, setSelectedSongList] = useState(null); + const [expandedSongKey, setExpandedSongKey] = useState(null); + - // Debug logging - only log when data changes - useEffect(() => { - console.log('SongLists component - songList count:', songListCount); - console.log('SongLists component - songLists:', songLists); - }, [songListCount, songLists.length]); const handleSongListClick = (songListKey: string) => { - console.log('SongLists - handleSongListClick called with key:', songListKey); setSelectedSongList(songListKey); + setExpandedSongKey(null); // Reset expansion when opening a new song list }; const handleCloseSongList = () => { setSelectedSongList(null); + setExpandedSongKey(null); // Reset expansion when closing + }; + + const handleSongItemClick = (songKey: string) => { + setExpandedSongKey(expandedSongKey === songKey ? null : songKey); }; const finalSelectedList = selectedSongList ? allSongLists.find(list => list.key === selectedSongList) : null; - - // Debug logging for modal - useEffect(() => { - console.log('SongLists - Modal state check:', { - selectedSongList, - finalSelectedList: !!finalSelectedList, - songListsLength: songLists.length - }); - if (selectedSongList) { - console.log('SongLists - Modal opened for song list:', selectedSongList); - console.log('SongLists - Selected list data:', finalSelectedList); - console.log('SongLists - About to render modal, finalSelectedList:', !!finalSelectedList); - } - }, [selectedSongList, finalSelectedList, songLists.length]); + + // Render song list item for InfiniteScrollList + const renderSongListItem = (songList: SongList) => ( + handleSongListClick(songList.key!)} detail={false}> + +

+ {songList.title} +

+

+ {songList.songs.length} song{songList.songs.length !== 1 ? 's' : ''} +

+
+ +
+ ); return ( <> - - - - Song Lists - - {songLists.length} - - - - + -
-

- {songLists.length} song list{songLists.length !== 1 ? 's' : ''} available -

- - {/* Debug info */} -
- Song lists loaded: {songListCount} -
+
+ + items={songLists} + isLoading={songListCount === 0} + hasMore={hasMore} + onLoadMore={loadMore} + renderItem={renderSongListItem} + emptyTitle="No song lists available" + emptyMessage="Song lists will appear here when they're available" + loadingTitle="Loading song lists..." + loadingMessage="Please wait while song lists are being loaded" + /> - {/* Song Lists */} -
- {songListCount === 0 ? ( -
-
- - - -
-

Loading song lists...

-

Please wait while song lists are being loaded

-
- ) : songLists.length === 0 ? ( -
-
- - - -
-

No song lists available

-

Song lists will appear here when they're available

-
- ) : ( - - {songLists.map((songList) => ( - handleSongListClick(songList.key!)}> - - -

- {songList.title} -

-

- {songList.songs.length} song{songList.songs.length !== 1 ? 's' : ''} -

-
- - View Songs - -
- ))} - - {/* Infinite scroll trigger */} - {hasMore && ( -
-
- - - - - Loading more song lists... -
-
- )} -
- )} -
-
+ {/* Song List Modal */} + + + + {finalSelectedList?.title} + + + + + + + + + {finalSelectedList?.songs.map((songListSong: SongListSong, index) => { + const availableSongs = checkSongAvailability(songListSong); + const isAvailable = availableSongs.length > 0; + const songKey = songListSong.key || `${songListSong.title}-${songListSong.position}-${index}`; - {/* Song List Modal */} - - - - {finalSelectedList?.title} - - - - - - {/* Remove IonContent, use a div instead */} -
- - {finalSelectedList?.songs.map((songListSong: SongListSong, idx) => { - const availableSongs = checkSongAvailability(songListSong); - const isAvailable = availableSongs.length > 0; + if (isAvailable) { + // Available songs get an accordion that expands + return ( + + handleSongItemClick(songKey)} + style={{ + '--border-style': 'solid', + '--border-width': '0 0 1px 0', + '--border-color': 'rgba(0, 0, 0, 0.13)' + } as React.CSSProperties} + > + {/* Number */} +
+ {index + 1}) +
- return ( - - - -

- {songListSong.title} -

-

- {songListSong.artist} • Position {songListSong.position} -

- {!isAvailable && ( -

- Not available in catalog -

- )} -
- {isAvailable && ( - - {availableSongs.length} version{availableSongs.length !== 1 ? 's' : ''} - - )} -
+ +

+ {songListSong.artist} +

+

+ {songListSong.title} +

+
-
- {isAvailable ? ( - - {availableSongs.map((song: Song, sidx) => ( - - -

- {song.title} -

-

- {song.artist} -

-
-
- handleAddToQueue(song)} - > - - - handleToggleFavorite(song)} - > - - -
-
- ))} -
- ) : ( -
- No matching songs found in catalog + + {availableSongs.length} version{availableSongs.length !== 1 ? 's' : ''} + + + +
+ + {availableSongs.map((song: Song, sidx) => ( + handleAddToQueue(song)} + onToggleFavorite={() => handleToggleFavorite(song)} + /> + ))} +
- )} -
- - ); - })} - -
- +
+ ); + } else { + // Unavailable songs get a simple item + return ( + + {/* Number */} +
+ {index + 1}) +
+ + +

+ {songListSong.artist} +

+

+ {songListSong.title} +

+
+
+ ); + } + })} +
+ + +
); }; diff --git a/src/features/TopPlayed/Top100.tsx b/src/features/TopPlayed/Top100.tsx index 5491cfe..8a552b6 100644 --- a/src/features/TopPlayed/Top100.tsx +++ b/src/features/TopPlayed/Top100.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react'; import { useTopPlayed } from '../../hooks'; import { useAppSelector } from '../../redux'; import { selectTopPlayed } from '../../redux'; @@ -99,17 +98,6 @@ const Top100: React.FC = () => { return ( <> - - - - Top 100 Played - - {displayItems.length} - - - - -