diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 33b5364..17361ef 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,15 +1,14 @@ import React, { useState, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { IonApp, IonHeader, IonToolbar, IonTitle, IonContent, IonChip, IonMenuButton } from '@ionic/react'; -import { selectCurrentSinger, selectIsAdmin, selectControllerName } from '../../redux/authSlice'; +import { IonApp, IonHeader, IonToolbar, IonTitle, IonContent, IonMenuButton, IonIcon } from '@ionic/react'; +import { logOut } from 'ionicons/icons'; +import { selectControllerName } from '../../redux/authSlice'; import { logout } from '../../redux/authSlice'; import { ActionButton } from '../common'; import Navigation from '../Navigation/Navigation'; import type { LayoutProps } from '../../types'; const Layout: React.FC = ({ children }) => { - const currentSinger = useSelector(selectCurrentSinger); - const isAdmin = useSelector(selectIsAdmin); const controllerName = useSelector(selectControllerName); const dispatch = useDispatch(); const [isLargeScreen, setIsLargeScreen] = useState(false); @@ -52,35 +51,25 @@ const Layout: React.FC = ({ children }) => {
- 🎤 Karaoke App + Sings A Lot {controllerName && ( - Party: {controllerName} + : {controllerName} )}
- {/* User Info & Logout */} - {currentSinger && ( -
-
- {currentSinger} - {isAdmin && ( - - Admin - - )} -
- - Logout - -
- )} + {/* Logout Button */} +
+ + + +
diff --git a/src/components/common/InfiniteScrollList.tsx b/src/components/common/InfiniteScrollList.tsx index 140909c..2da9c22 100644 --- a/src/components/common/InfiniteScrollList.tsx +++ b/src/components/common/InfiniteScrollList.tsx @@ -7,28 +7,22 @@ interface InfiniteScrollListProps { hasMore: boolean; onLoadMore: () => void; renderItem: (item: T, index: number) => React.ReactNode; - title: string; - subtitle?: string; emptyTitle: string; emptyMessage: string; loadingTitle?: string; loadingMessage?: string; - debugInfo?: string; } -const InfiniteScrollList = ({ +const InfiniteScrollList = ({ items, isLoading, hasMore, onLoadMore, renderItem, - title, - subtitle, emptyTitle, emptyMessage, loadingTitle = "Loading...", loadingMessage = "Please wait while data is being loaded", - debugInfo, }: InfiniteScrollListProps) => { const observerRef = useRef(null); @@ -59,22 +53,16 @@ const InfiniteScrollList = ({ return () => observer.disconnect(); }, [onLoadMore, hasMore, isLoading, items.length]); + // Generate key for item + const getItemKey = (item: T, index: number): string => { + if (typeof item === 'string') { + return item; + } + return item.key || `item-${index}`; + }; + return (
-
-

{title}

- {subtitle && ( -

{subtitle}

- )} - - {/* Debug info */} - {debugInfo && ( -
- {debugInfo} -
- )} -
- {/* List */}
{items.length === 0 && !isLoading ? ( @@ -98,9 +86,9 @@ const InfiniteScrollList = ({ } /> ) : ( -
+
{items.map((item, index) => ( -
+
{renderItem(item, index)}
))} diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx new file mode 100644 index 0000000..e8e6f6a --- /dev/null +++ b/src/components/common/PageHeader.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +interface PageHeaderProps { + title: string; + subtitle?: string; +} + +const PageHeader: React.FC = ({ title, subtitle }) => { + return ( +
+
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+
+ ); +}; + +export default PageHeader; \ No newline at end of file diff --git a/src/components/common/SongItem.tsx b/src/components/common/SongItem.tsx index 8c3ba18..12c108b 100644 --- a/src/components/common/SongItem.tsx +++ b/src/components/common/SongItem.tsx @@ -1,6 +1,9 @@ import React from 'react'; -import { IonItem, IonLabel } from '@ionic/react'; +import { IonItem, IonLabel, IonIcon } from '@ionic/react'; +import { add, heart, heartOutline, trash } from 'ionicons/icons'; import ActionButton from './ActionButton'; +import { useAppSelector } from '../../redux'; +import { selectQueue, selectFavorites } from '../../redux'; import type { SongItemProps } from '../../types'; // Utility function to extract filename from path @@ -23,127 +26,91 @@ const SongItem: React.FC = ({ isAdmin = false, className = '' }) => { + // Get current state from Redux + const queue = useAppSelector(selectQueue); + const favorites = useAppSelector(selectFavorites); + + // Check if song is in queue or favorites based on path + const isInQueue = Object.values(queue).some(item => item.song.path === song.path); + const isInFavorites = Object.values(favorites).some(favSong => favSong.path === song.path); const renderActionPanel = () => { - switch (context) { - case 'search': - return ( -
- {})} - variant="primary" - size="sm" - > - Add to Queue - - {})} - variant={song.favorite ? 'danger' : 'secondary'} - size="sm" - > - {song.favorite ? '❤️' : '🤍'} - -
- ); - - case 'queue': - return ( -
- {isAdmin && onRemoveFromQueue && ( - - Remove - - )} - {})} - variant={song.favorite ? 'danger' : 'secondary'} - size="sm" - > - {song.favorite ? '❤️' : '🤍'} - -
- ); - - case 'history': - return ( -
- {})} - variant="primary" - size="sm" - > - Add to Queue - - {})} - variant={song.favorite ? 'danger' : 'secondary'} - size="sm" - > - {song.favorite ? '❤️' : '🤍'} - -
- ); - - case 'favorites': - return ( -
- {})} - variant="primary" - size="sm" - > - Add to Queue - - {})} - variant="danger" - size="sm" - > - Remove - -
- ); - - case 'topPlayed': - return ( -
- {})} - variant="primary" - size="sm" - > - Add to Queue - - {})} - variant={song.favorite ? 'danger' : 'secondary'} - size="sm" - > - {song.favorite ? '❤️' : '🤍'} - -
- ); - - default: - return null; + const buttons = []; + + // Add to Queue button (for all contexts except queue, only if not already in queue) + if (context !== 'queue' && !isInQueue) { + buttons.push( + {})} + variant="primary" + size="sm" + > + + + ); } + + // Remove from Queue button (only for queue context, admin only) + if (context === 'queue' && isAdmin && onRemoveFromQueue) { + buttons.push( + + + + ); + } + + // Delete from Favorites button (only for favorites context) + if (context === 'favorites' && onDelete) { + buttons.push( + + + + ); + } + + // Toggle Favorite button (for all contexts except favorites) + if (context !== 'favorites') { + buttons.push( + {})} + variant={isInFavorites ? 'danger' : 'secondary'} + size="sm" + > + + + ); + } + + return buttons.length > 0 ? ( +
+ {buttons} +
+ ) : null; }; return ( - -

+ +

{song.title}

-

+

{song.artist}

{/* Show filename for all contexts except queue */} {context !== 'queue' && song.path && ( -

+

{extractFilename(song.path)}

)} @@ -154,7 +121,7 @@ const SongItem: React.FC = ({ )}
-
+
{renderActionPanel()}
diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 017d369..99d5548 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -3,5 +3,6 @@ export { default as EmptyState } from './EmptyState'; export { default as Toast } from './Toast'; export { default as ErrorBoundary } from './ErrorBoundary'; export { default as InfiniteScrollList } from './InfiniteScrollList'; +export { default as PageHeader } from './PageHeader'; export { default as SongItem } from './SongItem'; export { default as PlayerControls } from './PlayerControls'; \ No newline at end of file diff --git a/src/features/Artists/Artists.tsx b/src/features/Artists/Artists.tsx index 7cacf6e..db2de08 100644 --- a/src/features/Artists/Artists.tsx +++ b/src/features/Artists/Artists.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonChip } from '@ionic/react'; -import { close, add, heart, heartOutline } from 'ionicons/icons'; +import React, { useState } from 'react'; +import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon } from '@ionic/react'; +import { close, add, heart, heartOutline, list } from 'ionicons/icons'; +import { InfiniteScrollList, PageHeader } from '../../components/common'; import { useArtists } from '../../hooks'; import { useAppSelector } from '../../redux'; import { selectSongs } from '../../redux'; @@ -8,7 +9,6 @@ import { selectSongs } from '../../redux'; const Artists: React.FC = () => { const { artists, - allArtists, searchTerm, hasMore, loadMore, @@ -22,40 +22,12 @@ const Artists: React.FC = () => { const songs = useAppSelector(selectSongs); const songsCount = Object.keys(songs).length; const [selectedArtist, setSelectedArtist] = useState(null); - const observerRef = useRef(null); - - // Intersection Observer for infinite scrolling - useEffect(() => { - console.log('Artists - Setting up observer:', { hasMore, songsCount, itemsLength: artists.length }); - - const observer = new IntersectionObserver( - (entries) => { - console.log('Artists - Intersection detected:', { - isIntersecting: entries[0].isIntersecting, - hasMore, - songsCount - }); - - if (entries[0].isIntersecting && hasMore && songsCount > 0) { - console.log('Artists - Loading more items'); - loadMore(); - } - }, - { threshold: 0.1 } - ); - - if (observerRef.current) { - observer.observe(observerRef.current); - } - - return () => observer.disconnect(); - }, [loadMore, hasMore, songsCount, artists.length]); // Debug logging - useEffect(() => { - console.log('Artists component - artists count:', artists.length); - console.log('Artists component - selected artist:', selectedArtist); - }, [artists.length, selectedArtist]); + console.log('Artists component - artists count:', artists.length); + console.log('Artists component - selected artist:', selectedArtist); + console.log('Artists component - songs count:', songsCount); + console.log('Artists component - search term:', searchTerm); const handleArtistClick = (artist: string) => { setSelectedArtist(artist); @@ -67,6 +39,21 @@ const Artists: React.FC = () => { const selectedArtistSongs = selectedArtist ? getSongsByArtist(selectedArtist) : []; + // Render artist item for InfiniteScrollList + const renderArtistItem = (artist: string) => ( + handleArtistClick(artist)} detail={false}> + +

+ {artist} +

+

+ {getSongCountByArtist(artist)} song{getSongCountByArtist(artist) !== 1 ? 's' : ''} +

+
+ +
+ ); + return ( <> @@ -74,10 +61,14 @@ const Artists: React.FC = () => { Artists + + +
-

Artists

- {/* Search Input */} { debounce={300} showClearButton="focus" /> - - {/* Debug info */} -
- Total songs loaded: {songsCount} | Showing: {artists.length} of {allArtists.length} artists | Search term: "{searchTerm}" -
{/* Artists List */} -
- {songsCount === 0 ? ( -
-
- - - -
-

Loading artists...

-

Please wait while songs are being loaded from the database

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

- {searchTerm ? "No artists found" : "No artists available"} -

-

- {searchTerm ? "Try adjusting your search terms" : "Artists will appear here once songs are loaded"} -

-
- ) : ( - - {artists.map((artist) => ( - handleArtistClick(artist)}> - -

- {artist} -

-

- {getSongCountByArtist(artist)} song{getSongCountByArtist(artist) !== 1 ? 's' : ''} -

-
- - View Songs - -
- ))} - - {/* Infinite scroll trigger */} - {hasMore && ( -
-
- - - - - Loading more artists... -
-
- )} -
- )} -
+ + items={artists} + isLoading={songsCount === 0} + hasMore={hasMore} + onLoadMore={loadMore} + renderItem={renderArtistItem} + emptyTitle={searchTerm ? "No artists found" : "No artists available"} + emptyMessage={searchTerm ? "Try adjusting your search terms" : "Artists will appear here once songs are loaded"} + loadingTitle="Loading artists..." + loadingMessage="Please wait while songs are being loaded from the database" + /> {/* Artist Songs Modal */} diff --git a/src/features/Favorites/Favorites.tsx b/src/features/Favorites/Favorites.tsx index cb0d468..9e91ea9 100644 --- a/src/features/Favorites/Favorites.tsx +++ b/src/features/Favorites/Favorites.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react'; -import { InfiniteScrollList, SongItem } from '../../components/common'; +import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common'; import { useFavorites } from '../../hooks'; import { useAppSelector } from '../../redux'; import { selectFavorites } from '../../redux'; @@ -35,6 +35,11 @@ const Favorites: React.FC = () => { + + items={favoritesItems} isLoading={favoritesCount === 0} @@ -48,13 +53,10 @@ const Favorites: React.FC = () => { onToggleFavorite={() => handleToggleFavorite(song)} /> )} - title="Favorites" - subtitle={`${favoritesCount} items loaded`} emptyTitle="No favorites yet" emptyMessage="Add songs to your favorites to see them here" loadingTitle="Loading favorites..." loadingMessage="Please wait while favorites data is being loaded" - debugInfo={`Favorites items loaded: ${favoritesCount}`} /> ); diff --git a/src/features/History/History.tsx b/src/features/History/History.tsx index 33cdffe..25089c9 100644 --- a/src/features/History/History.tsx +++ b/src/features/History/History.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { IonHeader, IonToolbar, IonTitle, IonChip, IonIcon } from '@ionic/react'; import { time } from 'ionicons/icons'; -import { InfiniteScrollList, SongItem } from '../../components/common'; +import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common'; import { useHistory } from '../../hooks'; import { useAppSelector } from '../../redux'; import { selectHistory } from '../../redux'; @@ -50,6 +50,11 @@ const History: React.FC = () => { + +
items={historyItems} @@ -69,13 +74,10 @@ const History: React.FC = () => { {renderExtraContent(song)}
)} - title="Recently Played" - subtitle={`${historyCount} items loaded`} emptyTitle="No history yet" emptyMessage="Songs will appear here after they've been played" loadingTitle="Loading history..." loadingMessage="Please wait while history data is being loaded" - debugInfo={`History items loaded: ${historyCount}`} />
diff --git a/src/features/NewSongs/NewSongs.tsx b/src/features/NewSongs/NewSongs.tsx index 581f9d6..eda3f33 100644 --- a/src/features/NewSongs/NewSongs.tsx +++ b/src/features/NewSongs/NewSongs.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react'; -import { InfiniteScrollList, SongItem } from '../../components/common'; +import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common'; import { useNewSongs } from '../../hooks'; import { useAppSelector } from '../../redux'; import { selectNewSongs } from '../../redux'; @@ -35,6 +35,11 @@ const NewSongs: React.FC = () => { + + items={newSongsItems} isLoading={newSongsCount === 0} @@ -48,13 +53,10 @@ const NewSongs: React.FC = () => { onToggleFavorite={() => handleToggleFavorite(song)} /> )} - title="New Songs" - subtitle={`${newSongsCount} items loaded`} emptyTitle="No new songs" emptyMessage="Check back later for new additions" loadingTitle="Loading new songs..." loadingMessage="Please wait while new songs data is being loaded" - debugInfo={`New songs loaded: ${newSongsCount}`} /> ); diff --git a/src/features/Queue/Queue.tsx b/src/features/Queue/Queue.tsx index 304571e..3a99f69 100644 --- a/src/features/Queue/Queue.tsx +++ b/src/features/Queue/Queue.tsx @@ -74,7 +74,7 @@ const Queue: React.FC = () => { } /> ) : ( - + {queueItems.map((queueItem, index) => { console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`); const canDelete = index === 0 ? canDeleteFirstItem : true; diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index 36efe69..f9ebf14 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { IonSearchbar } from '@ionic/react'; -import { InfiniteScrollList, SongItem } from '../../components/common'; +import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common'; import { useSearch } from '../../hooks'; import { useAppSelector } from '../../redux'; import { selectIsAdmin, selectSongs } from '../../redux'; @@ -37,9 +37,12 @@ const Search: React.FC = () => { return (
+ +
-

Search Songs

- {/* Search Input */} { isAdmin={isAdmin} /> )} - title="" emptyTitle={searchTerm ? "No songs found" : "No songs available"} emptyMessage={searchTerm ? "Try adjusting your search terms" : "Songs will appear here once loaded"} loadingTitle="Loading songs..." loadingMessage="Please wait while songs are being loaded from the database" - debugInfo="" /> {/* Search Stats */} diff --git a/src/features/TopPlayed/Top100.tsx b/src/features/TopPlayed/Top100.tsx index 92f603c..5491cfe 100644 --- a/src/features/TopPlayed/Top100.tsx +++ b/src/features/TopPlayed/Top100.tsx @@ -3,7 +3,7 @@ import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react'; import { useTopPlayed } from '../../hooks'; import { useAppSelector } from '../../redux'; import { selectTopPlayed } from '../../redux'; -import { InfiniteScrollList } from '../../components/common'; +import { InfiniteScrollList, PageHeader } from '../../components/common'; import type { TopPlayed } from '../../types'; const Top100: React.FC = () => { @@ -110,6 +110,11 @@ const Top100: React.FC = () => { + + items={displayItems} isLoading={false} @@ -133,11 +138,8 @@ const Top100: React.FC = () => {
)} - title="Top 100 Played" - subtitle={`${displayCount} items loaded (Mock Data)`} emptyTitle="No top played songs" emptyMessage="Play some songs to see the top played list" - debugInfo={`Top played items loaded: ${displayCount} (Mock Data)`} /> );