Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
0bbd36010a
commit
4a5acb5a79
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
Loading…
Reference in New Issue
Block a user