Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-07-17 17:41:06 -05:00
parent 0bbd36010a
commit 4a5acb5a79
6 changed files with 441 additions and 339 deletions

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonContent, IonButton, IonIcon, IonChip } from '@ionic/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 { close, add, heart, heartOutline } from 'ionicons/icons';
import { useArtists } from '../../hooks'; import { useArtists } from '../../hooks';
import { useAppSelector } from '../../redux'; import { useAppSelector } from '../../redux';
@ -68,133 +68,140 @@ const Artists: React.FC = () => {
const selectedArtistSongs = selectedArtist ? getSongsByArtist(selectedArtist) : []; const selectedArtistSongs = selectedArtist ? getSongsByArtist(selectedArtist) : [];
return ( return (
<div className="max-w-4xl mx-auto p-6"> <>
<div className="mb-6"> <IonHeader>
<h1 className="text-2xl font-bold text-gray-900 mb-4">Artists</h1> <IonToolbar>
<IonTitle>Artists</IonTitle>
{/* Search Input */} </IonToolbar>
<IonSearchbar </IonHeader>
placeholder="Search artists..." <div className="max-w-4xl mx-auto p-6">
value={searchTerm} <div className="mb-6">
onIonInput={(e) => handleSearchChange(e.detail.value || '')} <h1 className="text-2xl font-bold text-gray-900 mb-4">Artists</h1>
debounce={300}
showClearButton="focus" {/* Search Input */}
/> <IonSearchbar
placeholder="Search artists..."
value={searchTerm}
onIonInput={(e) => handleSearchChange(e.detail.value || '')}
debounce={300}
showClearButton="focus"
/>
{/* Debug info */} {/* Debug info */}
<div className="mt-2 text-sm text-gray-500"> <div className="mt-2 text-sm text-gray-500">
Total songs loaded: {songsCount} | Showing: {artists.length} of {allArtists.length} artists | Search term: "{searchTerm}" Total songs loaded: {songsCount} | Showing: {artists.length} of {allArtists.length} artists | Search term: "{searchTerm}"
</div>
</div> </div>
</div>
{/* Artists List */} {/* Artists List */}
<div className="bg-white rounded-lg shadow"> <div className="bg-white rounded-lg shadow">
{songsCount === 0 ? ( {songsCount === 0 ? (
<div className="p-8 text-center"> <div className="p-8 text-center">
<div className="text-gray-400 mb-4"> <div className="text-gray-400 mb-4">
<svg className="h-12 w-12 mx-auto animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-12 w-12 mx-auto animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Loading artists...</h3>
<p className="text-sm text-gray-500">Please wait while songs are being loaded from the database</p>
</div>
) : artists.length === 0 ? (
<div className="p-8 text-center">
<div className="text-gray-400 mb-4">
<svg className="h-12 w-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
{searchTerm ? "No artists found" : "No artists available"}
</h3>
<p className="text-sm text-gray-500">
{searchTerm ? "Try adjusting your search terms" : "Artists will appear here once songs are loaded"}
</p>
</div>
) : (
<IonList>
{artists.map((artist) => (
<IonItem key={artist} button onClick={() => handleArtistClick(artist)}>
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
{artist}
</h3>
<p className="text-sm text-gray-500">
{getSongCountByArtist(artist)} song{getSongCountByArtist(artist) !== 1 ? 's' : ''}
</p>
</IonLabel>
<IonChip slot="end" color="primary">
View Songs
</IonChip>
</IonItem>
))}
{/* Infinite scroll trigger */}
{hasMore && (
<div
ref={observerRef}
className="py-4 text-center text-gray-500"
>
<div className="inline-flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading more artists...
</div>
</div> </div>
)} <h3 className="text-lg font-medium text-gray-900 mb-2">Loading artists...</h3>
</IonList> <p className="text-sm text-gray-500">Please wait while songs are being loaded from the database</p>
)} </div>
</div> ) : artists.length === 0 ? (
<div className="p-8 text-center">
{/* Artist Songs Modal */} <div className="text-gray-400 mb-4">
<IonModal isOpen={!!selectedArtist} onDidDismiss={handleCloseArtistSongs}> <svg className="h-12 w-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<IonHeader> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
<IonToolbar> </svg>
<IonTitle>Songs by {selectedArtist}</IonTitle> </div>
<IonButton slot="end" fill="clear" onClick={handleCloseArtistSongs}> <h3 className="text-lg font-medium text-gray-900 mb-2">
<IonIcon icon={close} /> {searchTerm ? "No artists found" : "No artists available"}
</IonButton> </h3>
</IonToolbar> <p className="text-sm text-gray-500">
</IonHeader> {searchTerm ? "Try adjusting your search terms" : "Artists will appear here once songs are loaded"}
</p>
<IonContent> </div>
<IonList> ) : (
{selectedArtistSongs.map((song) => ( <IonList>
<IonItem key={song.key}> {artists.map((artist) => (
<IonLabel> <IonItem key={artist} button onClick={() => handleArtistClick(artist)}>
<h3 className="text-sm font-medium text-gray-900"> <IonLabel>
{song.title} <h3 className="text-sm font-medium text-gray-900">
</h3> {artist}
<p className="text-sm text-gray-500"> </h3>
{song.artist} <p className="text-sm text-gray-500">
</p> {getSongCountByArtist(artist)} song{getSongCountByArtist(artist) !== 1 ? 's' : ''}
</IonLabel> </p>
<div slot="end" className="flex gap-2"> </IonLabel>
<IonButton <IonChip slot="end" color="primary">
fill="clear" View Songs
size="small" </IonChip>
onClick={() => handleAddToQueue(song)} </IonItem>
> ))}
<IonIcon icon={add} slot="icon-only" />
</IonButton> {/* Infinite scroll trigger */}
<IonButton {hasMore && (
fill="clear" <div
size="small" ref={observerRef}
onClick={() => handleToggleFavorite(song)} className="py-4 text-center text-gray-500"
> >
<IonIcon icon={song.favorite ? heart : heartOutline} slot="icon-only" /> <div className="inline-flex items-center">
</IonButton> <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading more artists...
</div>
</div> </div>
</IonItem> )}
))} </IonList>
</IonList> )}
</IonContent> </div>
</IonModal>
</div> {/* Artist Songs Modal */}
<IonModal isOpen={!!selectedArtist} onDidDismiss={handleCloseArtistSongs}>
<IonHeader>
<IonToolbar>
<IonTitle>Songs by {selectedArtist}</IonTitle>
<IonButton slot="end" fill="clear" onClick={handleCloseArtistSongs}>
<IonIcon icon={close} />
</IonButton>
</IonToolbar>
</IonHeader>
<div className="p-4">
<IonList>
{selectedArtistSongs.map((song) => (
<IonItem key={song.key}>
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
{song.title}
</h3>
<p className="text-sm text-gray-500">
{song.artist}
</p>
</IonLabel>
<div slot="end" className="flex gap-2">
<IonButton
fill="clear"
size="small"
onClick={() => handleAddToQueue(song)}
>
<IonIcon icon={add} slot="icon-only" />
</IonButton>
<IonButton
fill="clear"
size="small"
onClick={() => handleToggleFavorite(song)}
>
<IonIcon icon={song.favorite ? heart : heartOutline} slot="icon-only" />
</IonButton>
</div>
</IonItem>
))}
</IonList>
</div>
</IonModal>
</div>
</>
); );
}; };

View File

@ -34,24 +34,22 @@ const Favorites: React.FC = () => {
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<div style={{ height: '100%', overflowY: 'auto' }}> <InfiniteScrollList
<InfiniteScrollList items={favoritesItems}
items={favoritesItems} isLoading={favoritesCount === 0}
isLoading={favoritesCount === 0} hasMore={hasMore}
hasMore={hasMore} onLoadMore={loadMore}
onLoadMore={loadMore} onAddToQueue={handleAddToQueue}
onAddToQueue={handleAddToQueue} onToggleFavorite={handleToggleFavorite}
onToggleFavorite={handleToggleFavorite} context="favorites"
context="favorites" title=""
title="" subtitle=""
subtitle="" emptyTitle="No favorites yet"
emptyTitle="No favorites yet" emptyMessage="Add songs to your favorites to see them here"
emptyMessage="Add songs to your favorites to see them here" loadingTitle="Loading favorites..."
loadingTitle="Loading favorites..." loadingMessage="Please wait while favorites data is being loaded"
loadingMessage="Please wait while favorites data is being loaded" debugInfo={`Favorites items loaded: ${favoritesCount}`}
debugInfo={`Favorites items loaded: ${favoritesCount}`} />
/>
</div>
</> </>
); );
}; };

View File

@ -34,24 +34,22 @@ const NewSongs: React.FC = () => {
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<div style={{ height: '100%', overflowY: 'auto' }}> <InfiniteScrollList
<InfiniteScrollList items={newSongsItems}
items={newSongsItems} isLoading={newSongsCount === 0}
isLoading={newSongsCount === 0} hasMore={hasMore}
hasMore={hasMore} onLoadMore={loadMore}
onLoadMore={loadMore} onAddToQueue={handleAddToQueue}
onAddToQueue={handleAddToQueue} onToggleFavorite={handleToggleFavorite}
onToggleFavorite={handleToggleFavorite} context="search"
context="search" title=""
title="" subtitle=""
subtitle="" emptyTitle="No new songs"
emptyTitle="No new songs" emptyMessage="Check back later for new additions"
emptyMessage="Check back later for new additions" loadingTitle="Loading new songs..."
loadingTitle="Loading new songs..." loadingMessage="Please wait while new songs data is being loaded"
loadingMessage="Please wait while new songs data is being loaded" debugInfo={`New songs loaded: ${newSongsCount}`}
debugInfo={`New songs loaded: ${newSongsCount}`} />
/>
</div>
</> </>
); );
}; };

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel, IonItemSliding, IonItemOptions, IonItemOption, IonIcon, IonChip } from '@ionic/react'; import { IonHeader, IonToolbar, IonTitle, IonList, IonItem, IonLabel, IonItemSliding, IonItemOptions, IonItemOption, IonIcon, IonChip } from '@ionic/react';
import { people, trash, time } from 'ionicons/icons'; import { people, trash, time } from 'ionicons/icons';
import { EmptyState } from '../../components/common'; import { EmptyState } from '../../components/common';
import { useSingers } from '../../hooks'; import { useSingers } from '../../hooks';
@ -34,76 +34,74 @@ const Singers: React.FC = () => {
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<IonContent> <div className="p-4">
<div className="p-4"> <p className="text-sm text-gray-600 mb-4">
<p className="text-sm text-gray-600 mb-4"> {singers.length} singer{singers.length !== 1 ? 's' : ''} in the party
{singers.length} singer{singers.length !== 1 ? 's' : ''} in the party </p>
</p>
{/* Debug info */}
{/* Debug info */} <div className="mb-4 text-sm text-gray-500">
<div className="mb-4 text-sm text-gray-500"> Singers loaded: {singersCount}
Singers loaded: {singersCount}
</div>
{/* Singers List */}
<div className="bg-white rounded-lg shadow">
{singersCount === 0 ? (
<EmptyState
title="No singers yet"
message="Singers will appear here when they join the party"
icon={
<svg className="h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
}
/>
) : singers.length === 0 ? (
<EmptyState
title="Loading singers..."
message="Please wait while singers data is being loaded"
icon={
<svg className="h-12 w-12 text-gray-400 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
}
/>
) : (
<IonList>
{singers.map((singer) => (
<IonItemSliding key={singer.key}>
<IonItem>
<IonIcon icon={people} slot="start" color="primary" />
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
{singer.name}
</h3>
<div className="flex items-center mt-1">
<IonChip color="medium">
<IonIcon icon={time} />
{formatDate(singer.lastLogin)}
</IonChip>
</div>
</IonLabel>
</IonItem>
{/* Swipe to Remove (Admin Only) */}
{isAdmin && (
<IonItemOptions side="end">
<IonItemOption
color="danger"
onClick={() => handleRemoveSinger(singer)}
>
<IonIcon icon={trash} slot="icon-only" />
</IonItemOption>
</IonItemOptions>
)}
</IonItemSliding>
))}
</IonList>
)}
</div>
</div> </div>
</IonContent>
{/* Singers List */}
<div className="bg-white rounded-lg shadow">
{singersCount === 0 ? (
<EmptyState
title="No singers yet"
message="Singers will appear here when they join the party"
icon={
<svg className="h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
}
/>
) : singers.length === 0 ? (
<EmptyState
title="Loading singers..."
message="Please wait while singers data is being loaded"
icon={
<svg className="h-12 w-12 text-gray-400 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
}
/>
) : (
<IonList>
{singers.map((singer) => (
<IonItemSliding key={singer.key}>
<IonItem>
<IonIcon icon={people} slot="start" color="primary" />
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
{singer.name}
</h3>
<div className="flex items-center mt-1">
<IonChip color="medium">
<IonIcon icon={time} />
{formatDate(singer.lastLogin)}
</IonChip>
</div>
</IonLabel>
</IonItem>
{/* Swipe to Remove (Admin Only) */}
{isAdmin && (
<IonItemOptions side="end">
<IonItemOption
color="danger"
onClick={() => handleRemoveSinger(singer)}
>
<IonIcon icon={trash} slot="icon-only" />
</IonItemOption>
</IonItemOptions>
)}
</IonItemSliding>
))}
</IonList>
)}
</div>
</div>
</> </>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel, IonModal, IonButton, IonIcon, IonChip, IonAccordion, IonAccordionGroup } from '@ionic/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 { close, documentText, add, heart, heartOutline } from 'ionicons/icons';
import { useSongLists } from '../../hooks'; import { useSongLists } from '../../hooks';
import { useAppSelector } from '../../redux'; import { useAppSelector } from '../../redux';
@ -95,78 +95,76 @@ const SongLists: React.FC = () => {
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<IonContent> <div className="p-4">
<div className="p-4"> <p className="text-sm text-gray-600 mb-4">
<p className="text-sm text-gray-600 mb-4"> {songLists.length} song list{songLists.length !== 1 ? 's' : ''} available
{songLists.length} song list{songLists.length !== 1 ? 's' : ''} available </p>
</p>
{/* Debug info */}
{/* Debug info */} <div className="mb-4 text-sm text-gray-500">
<div className="mb-4 text-sm text-gray-500"> Song lists loaded: {songListCount}
Song lists loaded: {songListCount}
</div>
{/* Song Lists */}
<div className="bg-white rounded-lg shadow">
{songListCount === 0 ? (
<div className="p-8 text-center">
<div className="text-gray-400 mb-4">
<svg className="h-12 w-12 mx-auto animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Loading song lists...</h3>
<p className="text-sm text-gray-500">Please wait while song lists are being loaded</p>
</div>
) : songLists.length === 0 ? (
<div className="p-8 text-center">
<div className="text-gray-400 mb-4">
<svg className="h-12 w-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No song lists available</h3>
<p className="text-sm text-gray-500">Song lists will appear here when they're available</p>
</div>
) : (
<IonList>
{songLists.map((songList) => (
<IonItem key={songList.key} button onClick={() => handleSongListClick(songList.key!)}>
<IonIcon icon={documentText} slot="start" color="primary" />
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
{songList.title}
</h3>
<p className="text-sm text-gray-500">
{songList.songs.length} song{songList.songs.length !== 1 ? 's' : ''}
</p>
</IonLabel>
<IonChip slot="end" color="primary">
View Songs
</IonChip>
</IonItem>
))}
{/* Infinite scroll trigger */}
{hasMore && (
<div
ref={observerRef}
className="py-4 text-center text-gray-500"
>
<div className="inline-flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading more song lists...
</div>
</div>
)}
</IonList>
)}
</div>
</div> </div>
</IonContent>
{/* Song Lists */}
<div className="bg-white rounded-lg shadow">
{songListCount === 0 ? (
<div className="p-8 text-center">
<div className="text-gray-400 mb-4">
<svg className="h-12 w-12 mx-auto animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Loading song lists...</h3>
<p className="text-sm text-gray-500">Please wait while song lists are being loaded</p>
</div>
) : songLists.length === 0 ? (
<div className="p-8 text-center">
<div className="text-gray-400 mb-4">
<svg className="h-12 w-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No song lists available</h3>
<p className="text-sm text-gray-500">Song lists will appear here when they're available</p>
</div>
) : (
<IonList>
{songLists.map((songList) => (
<IonItem key={songList.key} button onClick={() => handleSongListClick(songList.key!)}>
<IonIcon icon={documentText} slot="start" color="primary" />
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
{songList.title}
</h3>
<p className="text-sm text-gray-500">
{songList.songs.length} song{songList.songs.length !== 1 ? 's' : ''}
</p>
</IonLabel>
<IonChip slot="end" color="primary">
View Songs
</IonChip>
</IonItem>
))}
{/* Infinite scroll trigger */}
{hasMore && (
<div
ref={observerRef}
className="py-4 text-center text-gray-500"
>
<div className="inline-flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading more song lists...
</div>
</div>
)}
</IonList>
)}
</div>
</div>
{/* Song List Modal */} {/* Song List Modal */}
<IonModal isOpen={!!finalSelectedList} onDidDismiss={handleCloseSongList}> <IonModal isOpen={!!finalSelectedList} onDidDismiss={handleCloseSongList}>
@ -178,8 +176,8 @@ const SongLists: React.FC = () => {
</IonButton> </IonButton>
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
{/* Remove IonContent, use a div instead */}
<IonContent> <div>
<IonAccordionGroup> <IonAccordionGroup>
{finalSelectedList?.songs.map((songListSong: SongListSong, idx) => { {finalSelectedList?.songs.map((songListSong: SongListSong, idx) => {
const availableSongs = checkSongAvailability(songListSong); const availableSongs = checkSongAvailability(songListSong);
@ -212,7 +210,7 @@ const SongLists: React.FC = () => {
{isAvailable ? ( {isAvailable ? (
<IonList> <IonList>
{availableSongs.map((song: Song, sidx) => ( {availableSongs.map((song: Song, sidx) => (
<IonItem key={song.key || `${song.title}-${song.artist}-${sidx}`}> <IonItem key={song.key || `${song.title}-${song.artist}-${sidx}`}>
<IonLabel> <IonLabel>
<h3 className="text-sm font-medium text-gray-900"> <h3 className="text-sm font-medium text-gray-900">
{song.title} {song.title}
@ -250,7 +248,7 @@ const SongLists: React.FC = () => {
); );
})} })}
</IonAccordionGroup> </IonAccordionGroup>
</IonContent> </div>
</IonModal> </IonModal>
</> </>
); );

View File

@ -1,14 +1,17 @@
import React from 'react'; import React from 'react';
import { IonHeader, IonToolbar, IonTitle, IonChip, IonIcon } from '@ionic/react'; import { IonHeader, IonToolbar, IonTitle, IonChip, IonIcon } from '@ionic/react';
import { trophy } from 'ionicons/icons';
import { InfiniteScrollList } from '../../components/common'; import { InfiniteScrollList } from '../../components/common';
import { useTopPlayed } from '../../hooks'; import { useTopPlayed } from '../../hooks';
import { useAppSelector } from '../../redux'; import { useAppSelector } from '../../redux';
import { selectTopPlayed } from '../../redux'; import { selectTopPlayed } from '../../redux';
import type { TopPlayed, Song } from '../../types';
const Top100: React.FC = () => {
console.log('Top100 component - RENDERING START');
const TopPlayed: React.FC = () => {
const { const {
topPlayedItems, topPlayedItems,
hasMore,
loadMore, loadMore,
handleAddToQueue, handleAddToQueue,
handleToggleFavorite, handleToggleFavorite,
@ -17,18 +20,110 @@ const TopPlayed: React.FC = () => {
const topPlayed = useAppSelector(selectTopPlayed); const topPlayed = useAppSelector(selectTopPlayed);
const topPlayedCount = Object.keys(topPlayed).length; const topPlayedCount = Object.keys(topPlayed).length;
// Debug logging console.log('Top100 component - Redux data:', { topPlayedCount, topPlayedItems: topPlayedItems.length });
console.log('TopPlayed component - top played count:', topPlayedCount);
console.log('TopPlayed component - top played items:', topPlayedItems);
const renderExtraContent = (item: any, index: number) => ( // Mock data for testing
<div className="flex items-center space-x-2 px-4 py-2"> const mockTopPlayedItems: TopPlayed[] = [
<div className="flex items-center text-sm text-gray-500"> {
<IonIcon icon="trophy" className="mr-1" /> key: 'mock-1',
<span>#{index + 1}</span> title: 'Bohemian Rhapsody',
</div> artist: 'Queen',
</div> count: 156
); },
{
key: 'mock-2',
title: 'Sweet Caroline',
artist: 'Neil Diamond',
count: 142
},
{
key: 'mock-3',
title: 'Don\'t Stop Believin\'',
artist: 'Journey',
count: 128
},
{
key: 'mock-4',
title: 'Livin\' on a Prayer',
artist: 'Bon Jovi',
count: 115
},
{
key: 'mock-5',
title: 'Wonderwall',
artist: 'Oasis',
count: 98
},
{
key: 'mock-6',
title: 'Hotel California',
artist: 'Eagles',
count: 87
},
{
key: 'mock-7',
title: 'Stairway to Heaven',
artist: 'Led Zeppelin',
count: 76
},
{
key: 'mock-8',
title: 'Imagine',
artist: 'John Lennon',
count: 65
},
{
key: 'mock-9',
title: 'Hey Jude',
artist: 'The Beatles',
count: 54
},
{
key: 'mock-10',
title: 'Yesterday',
artist: 'The Beatles',
count: 43
}
];
// Convert TopPlayed items to Song format for consistent UI
const songItems: Song[] = mockTopPlayedItems.map((item: TopPlayed) => ({
...item,
path: '', // TopPlayed doesn't have path
disabled: false,
favorite: false,
}));
// Use mock data for now
const displayItems = songItems;
const displayCount = songItems.length;
const displayHasMore = false; // No more mock data to load
console.log('Top100 component - Mock data:', {
displayItems: displayItems.length,
displayCount,
displayHasMore,
firstItem: displayItems[0]
});
console.log('Top100 component - About to render JSX');
// Wrapper functions to handle type conversion
const handleAddToQueueWrapper = (song: Song) => {
console.log('Top100 component - Add to Queue clicked:', song);
const topPlayedItem = mockTopPlayedItems.find(item => item.key === song.key);
if (topPlayedItem) {
handleAddToQueue(topPlayedItem);
}
};
const handleToggleFavoriteWrapper = (song: Song) => {
console.log('Top100 component - Remove clicked:', song);
const topPlayedItem = mockTopPlayedItems.find(item => item.key === song.key);
if (topPlayedItem) {
handleToggleFavorite(topPlayedItem);
}
};
return ( return (
<> <>
@ -37,33 +132,41 @@ const TopPlayed: React.FC = () => {
<IonTitle> <IonTitle>
Top 100 Played Top 100 Played
<IonChip color="primary" className="ml-2"> <IonChip color="primary" className="ml-2">
{topPlayedItems.length} {displayItems.length}
</IonChip> </IonChip>
</IonTitle> </IonTitle>
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<div style={{ height: '100%', overflowY: 'auto' }}> <InfiniteScrollList
<InfiniteScrollList items={displayItems}
items={topPlayedItems} isLoading={displayCount === 0}
isLoading={topPlayedCount === 0} hasMore={displayHasMore}
hasMore={hasMore} onLoadMore={loadMore}
onLoadMore={loadMore} onAddToQueue={handleAddToQueueWrapper}
onAddToQueue={handleAddToQueue} onToggleFavorite={handleToggleFavoriteWrapper}
onToggleFavorite={handleToggleFavorite} context="topPlayed"
context="topPlayed" title=""
title="" subtitle=""
subtitle="" emptyTitle="No top played songs"
emptyTitle="No top played songs" emptyMessage="Play some songs to see the top played list"
emptyMessage="Play some songs to see the top played list" loadingTitle="Loading top played songs..."
loadingTitle="Loading top played songs..." loadingMessage="Please wait while top played data is being loaded"
loadingMessage="Please wait while top played data is being loaded" debugInfo={`Top played items loaded: ${displayCount} (Mock Data)`}
debugInfo={`Top played items loaded: ${topPlayedCount}`} renderExtraContent={(item: Song, index: number) => (
renderExtraContent={renderExtraContent} <div className="flex items-center space-x-2 px-4 py-2">
/> <div className="flex items-center text-sm text-gray-500">
</div> <IonIcon icon={trophy} className="mr-1" />
<span>#{index + 1}</span>
</div>
<IonChip color="primary">
{item.count} play{item.count !== 1 ? 's' : ''}
</IonChip>
</div>
)}
/>
</> </>
); );
}; };
export default TopPlayed; export default Top100;