Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
1e632f813c
commit
2d5f8fdb8f
@ -1,46 +1,35 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { SongItem, EmptyState } from './index';
|
import { EmptyState } from './index';
|
||||||
import type { Song } from '../../types';
|
|
||||||
|
|
||||||
interface InfiniteScrollListProps {
|
interface InfiniteScrollListProps<T> {
|
||||||
items: Song[];
|
items: T[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
onLoadMore: () => void;
|
onLoadMore: () => void;
|
||||||
onAddToQueue: (song: Song) => void;
|
renderItem: (item: T, index: number) => React.ReactNode;
|
||||||
onToggleFavorite: (song: Song) => void;
|
|
||||||
onRemoveFromQueue?: (song: Song) => void;
|
|
||||||
context: 'search' | 'queue' | 'history' | 'topPlayed' | 'favorites';
|
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
emptyTitle: string;
|
emptyTitle: string;
|
||||||
emptyMessage: string;
|
emptyMessage: string;
|
||||||
loadingTitle?: string;
|
loadingTitle?: string;
|
||||||
loadingMessage?: string;
|
loadingMessage?: string;
|
||||||
isAdmin?: boolean;
|
|
||||||
renderExtraContent?: (item: Song, index: number) => React.ReactNode;
|
|
||||||
debugInfo?: string;
|
debugInfo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InfiniteScrollList: React.FC<InfiniteScrollListProps> = ({
|
const InfiniteScrollList = <T extends { key?: string }>({
|
||||||
items,
|
items,
|
||||||
isLoading,
|
isLoading,
|
||||||
hasMore,
|
hasMore,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
onAddToQueue,
|
renderItem,
|
||||||
onToggleFavorite,
|
|
||||||
onRemoveFromQueue,
|
|
||||||
context,
|
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
emptyTitle,
|
emptyTitle,
|
||||||
emptyMessage,
|
emptyMessage,
|
||||||
loadingTitle = "Loading...",
|
loadingTitle = "Loading...",
|
||||||
loadingMessage = "Please wait while data is being loaded",
|
loadingMessage = "Please wait while data is being loaded",
|
||||||
isAdmin = false,
|
|
||||||
renderExtraContent,
|
|
||||||
debugInfo,
|
debugInfo,
|
||||||
}) => {
|
}: InfiniteScrollListProps<T>) => {
|
||||||
const observerRef = useRef<HTMLDivElement>(null);
|
const observerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Intersection Observer for infinite scrolling
|
// Intersection Observer for infinite scrolling
|
||||||
@ -111,21 +100,8 @@ const InfiniteScrollList: React.FC<InfiniteScrollListProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-200">
|
<div className="divide-y divide-gray-200">
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<div key={item.key} className="flex items-center">
|
<div key={item.key}>
|
||||||
{/* Song Info */}
|
{renderItem(item, index)}
|
||||||
<div className="flex-1">
|
|
||||||
<SongItem
|
|
||||||
song={item}
|
|
||||||
context={context}
|
|
||||||
onAddToQueue={() => onAddToQueue(item)}
|
|
||||||
onToggleFavorite={() => onToggleFavorite(item)}
|
|
||||||
onRemoveFromQueue={onRemoveFromQueue ? () => onRemoveFromQueue(item) : undefined}
|
|
||||||
isAdmin={isAdmin}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Extra Content */}
|
|
||||||
{renderExtraContent && renderExtraContent(item, index)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
|
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
|
||||||
import { InfiniteScrollList } from '../../components/common';
|
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||||
import { useFavorites } from '../../hooks';
|
import { useFavorites } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectFavorites } from '../../redux';
|
import { selectFavorites } from '../../redux';
|
||||||
|
import type { Song } from '../../types';
|
||||||
|
|
||||||
const Favorites: React.FC = () => {
|
const Favorites: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@ -34,16 +35,21 @@ const Favorites: React.FC = () => {
|
|||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
<InfiniteScrollList
|
<InfiniteScrollList<Song>
|
||||||
items={favoritesItems}
|
items={favoritesItems}
|
||||||
isLoading={favoritesCount === 0}
|
isLoading={favoritesCount === 0}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
onLoadMore={loadMore}
|
onLoadMore={loadMore}
|
||||||
onAddToQueue={handleAddToQueue}
|
renderItem={(song) => (
|
||||||
onToggleFavorite={handleToggleFavorite}
|
<SongItem
|
||||||
|
song={song}
|
||||||
context="favorites"
|
context="favorites"
|
||||||
title=""
|
onAddToQueue={() => handleAddToQueue(song)}
|
||||||
subtitle=""
|
onToggleFavorite={() => handleToggleFavorite(song)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
title="Favorites"
|
||||||
|
subtitle={`${favoritesCount} items loaded`}
|
||||||
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..."
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
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 { time } from 'ionicons/icons';
|
import { time } from 'ionicons/icons';
|
||||||
import { InfiniteScrollList } from '../../components/common';
|
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||||
import { useHistory } from '../../hooks';
|
import { useHistory } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectHistory } from '../../redux';
|
import { selectHistory } from '../../redux';
|
||||||
@ -51,22 +51,31 @@ const History: React.FC = () => {
|
|||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'auto' }}>
|
<div style={{ height: '100%', overflowY: 'auto' }}>
|
||||||
<InfiniteScrollList
|
<InfiniteScrollList<Song>
|
||||||
items={historyItems}
|
items={historyItems}
|
||||||
isLoading={historyCount === 0}
|
isLoading={historyCount === 0}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
onLoadMore={loadMore}
|
onLoadMore={loadMore}
|
||||||
onAddToQueue={handleAddToQueue}
|
renderItem={(song) => (
|
||||||
onToggleFavorite={handleToggleFavorite}
|
<div className="flex items-center">
|
||||||
|
<div className="flex-1">
|
||||||
|
<SongItem
|
||||||
|
song={song}
|
||||||
context="history"
|
context="history"
|
||||||
title=""
|
onAddToQueue={() => handleAddToQueue(song)}
|
||||||
subtitle=""
|
onToggleFavorite={() => handleToggleFavorite(song)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{renderExtraContent(song)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
title="Recently Played"
|
||||||
|
subtitle={`${historyCount} items loaded`}
|
||||||
emptyTitle="No history yet"
|
emptyTitle="No history yet"
|
||||||
emptyMessage="Songs will appear here after they've been played"
|
emptyMessage="Songs will appear here after they've been played"
|
||||||
loadingTitle="Loading history..."
|
loadingTitle="Loading history..."
|
||||||
loadingMessage="Please wait while history data is being loaded"
|
loadingMessage="Please wait while history data is being loaded"
|
||||||
debugInfo={`History items loaded: ${historyCount}`}
|
debugInfo={`History items loaded: ${historyCount}`}
|
||||||
renderExtraContent={renderExtraContent}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
|
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
|
||||||
import { InfiniteScrollList } from '../../components/common';
|
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||||
import { useNewSongs } from '../../hooks';
|
import { useNewSongs } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectNewSongs } from '../../redux';
|
import { selectNewSongs } from '../../redux';
|
||||||
|
import type { Song } from '../../types';
|
||||||
|
|
||||||
const NewSongs: React.FC = () => {
|
const NewSongs: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@ -34,16 +35,21 @@ const NewSongs: React.FC = () => {
|
|||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
<InfiniteScrollList
|
<InfiniteScrollList<Song>
|
||||||
items={newSongsItems}
|
items={newSongsItems}
|
||||||
isLoading={newSongsCount === 0}
|
isLoading={newSongsCount === 0}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
onLoadMore={loadMore}
|
onLoadMore={loadMore}
|
||||||
onAddToQueue={handleAddToQueue}
|
renderItem={(song) => (
|
||||||
onToggleFavorite={handleToggleFavorite}
|
<SongItem
|
||||||
|
song={song}
|
||||||
context="search"
|
context="search"
|
||||||
title=""
|
onAddToQueue={() => handleAddToQueue(song)}
|
||||||
subtitle=""
|
onToggleFavorite={() => handleToggleFavorite(song)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
title="New Songs"
|
||||||
|
subtitle={`${newSongsCount} items loaded`}
|
||||||
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..."
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IonSearchbar } from '@ionic/react';
|
import { IonSearchbar } from '@ionic/react';
|
||||||
import { InfiniteScrollList } from '../../components/common';
|
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||||
import { useSearch } from '../../hooks';
|
import { useSearch } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectIsAdmin, selectSongs } from '../../redux';
|
import { selectIsAdmin, selectSongs } from '../../redux';
|
||||||
|
import type { Song } from '../../types';
|
||||||
|
|
||||||
const Search: React.FC = () => {
|
const Search: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@ -55,20 +56,25 @@ const Search: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Results */}
|
{/* Search Results */}
|
||||||
<InfiniteScrollList
|
<InfiniteScrollList<Song>
|
||||||
items={searchResults.songs}
|
items={searchResults.songs}
|
||||||
isLoading={songsCount === 0}
|
isLoading={songsCount === 0}
|
||||||
hasMore={searchResults.hasMore}
|
hasMore={searchResults.hasMore}
|
||||||
onLoadMore={loadMore}
|
onLoadMore={loadMore}
|
||||||
onAddToQueue={handleAddToQueue}
|
renderItem={(song) => (
|
||||||
onToggleFavorite={handleToggleFavorite}
|
<SongItem
|
||||||
|
song={song}
|
||||||
context="search"
|
context="search"
|
||||||
|
onAddToQueue={() => handleAddToQueue(song)}
|
||||||
|
onToggleFavorite={() => handleToggleFavorite(song)}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
title=""
|
title=""
|
||||||
emptyTitle={searchTerm ? "No songs found" : "No songs available"}
|
emptyTitle={searchTerm ? "No songs found" : "No songs available"}
|
||||||
emptyMessage={searchTerm ? "Try adjusting your search terms" : "Songs will appear here once loaded"}
|
emptyMessage={searchTerm ? "Try adjusting your search terms" : "Songs will appear here once loaded"}
|
||||||
loadingTitle="Loading songs..."
|
loadingTitle="Loading songs..."
|
||||||
loadingMessage="Please wait while songs are being loaded from the database"
|
loadingMessage="Please wait while songs are being loaded from the database"
|
||||||
isAdmin={isAdmin}
|
|
||||||
debugInfo=""
|
debugInfo=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IonHeader, IonToolbar, IonTitle, IonChip, IonButton } from '@ionic/react';
|
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
|
||||||
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 { InfiniteScrollList } from '../../components/common';
|
||||||
import type { TopPlayed } from '../../types';
|
import type { TopPlayed } from '../../types';
|
||||||
|
|
||||||
const Top100: React.FC = () => {
|
const Top100: React.FC = () => {
|
||||||
@ -109,38 +110,13 @@ const Top100: React.FC = () => {
|
|||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto p-6">
|
<InfiniteScrollList<TopPlayed>
|
||||||
{/* Debug info */}
|
items={displayItems}
|
||||||
<div className="mt-2 text-sm text-gray-500 mb-4">
|
isLoading={false}
|
||||||
Top played items loaded: {displayCount} (Mock Data)
|
hasMore={displayHasMore}
|
||||||
</div>
|
onLoadMore={loadMore}
|
||||||
|
renderItem={(item, index) => (
|
||||||
{/* Content */}
|
<div style={{ display: 'flex', alignItems: 'flex-start', padding: '12px 16px', borderBottom: '1px solid #e5e7eb' }}>
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
{displayCount === 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 top played songs...</h3>
|
|
||||||
<p className="text-sm text-gray-500">Please wait while top played data is being loaded</p>
|
|
||||||
</div>
|
|
||||||
) : displayItems.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 top played songs</h3>
|
|
||||||
<p className="text-sm text-gray-500">Play some songs to see the top played list</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="w-full">
|
|
||||||
{displayItems.map((item, index) => (
|
|
||||||
<div key={item.key} style={{ display: 'flex', alignItems: 'flex-start', padding: '12px 16px', borderBottom: '1px solid #e5e7eb' }}>
|
|
||||||
<div style={{ width: '80px', textAlign: 'right', paddingRight: '16px', flexShrink: 0 }}>
|
<div style={{ width: '80px', textAlign: 'right', paddingRight: '16px', flexShrink: 0 }}>
|
||||||
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#374151' }}>
|
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#374151' }}>
|
||||||
{index + 1})
|
{index + 1})
|
||||||
@ -156,31 +132,13 @@ const Top100: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Load more button */}
|
|
||||||
{displayHasMore && (
|
|
||||||
<div className="p-4 text-center">
|
|
||||||
<IonButton
|
|
||||||
fill="outline"
|
|
||||||
onClick={loadMore}
|
|
||||||
>
|
|
||||||
Load More
|
|
||||||
</IonButton>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
title="Top 100 Played"
|
||||||
)}
|
subtitle={`${displayCount} items loaded (Mock Data)`}
|
||||||
</div>
|
emptyTitle="No top played songs"
|
||||||
|
emptyMessage="Play some songs to see the top played list"
|
||||||
{/* Stats */}
|
debugInfo={`Top played items loaded: ${displayCount} (Mock Data)`}
|
||||||
{displayItems.length > 0 && (
|
/>
|
||||||
<div className="mt-4 text-sm text-gray-500 text-center">
|
|
||||||
Showing {displayItems.length} item{displayItems.length !== 1 ? 's' : ''}
|
|
||||||
{displayHasMore && ` • Click "Load More" to see more`}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user