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

This commit is contained in:
Matt Bruce 2025-07-18 08:43:22 -05:00
parent 2d5f8fdb8f
commit 97c1b1e030
12 changed files with 197 additions and 286 deletions

View File

@ -1,15 +1,14 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { IonApp, IonHeader, IonToolbar, IonTitle, IonContent, IonChip, IonMenuButton } from '@ionic/react'; import { IonApp, IonHeader, IonToolbar, IonTitle, IonContent, IonMenuButton, IonIcon } from '@ionic/react';
import { selectCurrentSinger, selectIsAdmin, selectControllerName } from '../../redux/authSlice'; import { logOut } from 'ionicons/icons';
import { selectControllerName } from '../../redux/authSlice';
import { logout } from '../../redux/authSlice'; import { logout } from '../../redux/authSlice';
import { ActionButton } from '../common'; import { ActionButton } from '../common';
import Navigation from '../Navigation/Navigation'; import Navigation from '../Navigation/Navigation';
import type { LayoutProps } from '../../types'; import type { LayoutProps } from '../../types';
const Layout: React.FC<LayoutProps> = ({ children }) => { const Layout: React.FC<LayoutProps> = ({ children }) => {
const currentSinger = useSelector(selectCurrentSinger);
const isAdmin = useSelector(selectIsAdmin);
const controllerName = useSelector(selectControllerName); const controllerName = useSelector(selectControllerName);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isLargeScreen, setIsLargeScreen] = useState(false); const [isLargeScreen, setIsLargeScreen] = useState(false);
@ -52,35 +51,25 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
<IonTitle> <IonTitle>
<div className="flex items-center"> <div className="flex items-center">
<span>🎤 Karaoke App</span> <span>Sings A Lot</span>
{controllerName && ( {controllerName && (
<span className="ml-4 text-sm text-gray-500"> <span className="ml-4 text-sm text-gray-500">
Party: {controllerName} : {controllerName}
</span> </span>
)} )}
</div> </div>
</IonTitle> </IonTitle>
{/* User Info & Logout */} {/* Logout Button */}
{currentSinger && ( <div slot="end">
<div slot="end" className="flex items-center space-x-3"> <ActionButton
<div className="text-sm text-gray-600"> onClick={handleLogout}
<span className="font-medium">{currentSinger}</span> variant="secondary"
{isAdmin && ( size="sm"
<IonChip color="primary"> >
Admin <IonIcon icon={logOut} />
</IonChip> </ActionButton>
)} </div>
</div>
<ActionButton
onClick={handleLogout}
variant="secondary"
size="sm"
>
Logout
</ActionButton>
</div>
)}
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>

View File

@ -7,28 +7,22 @@ interface InfiniteScrollListProps<T> {
hasMore: boolean; hasMore: boolean;
onLoadMore: () => void; onLoadMore: () => void;
renderItem: (item: T, index: number) => React.ReactNode; renderItem: (item: T, index: number) => React.ReactNode;
title: string;
subtitle?: string;
emptyTitle: string; emptyTitle: string;
emptyMessage: string; emptyMessage: string;
loadingTitle?: string; loadingTitle?: string;
loadingMessage?: string; loadingMessage?: string;
debugInfo?: string;
} }
const InfiniteScrollList = <T extends { key?: string }>({ const InfiniteScrollList = <T extends string | { key?: string }>({
items, items,
isLoading, isLoading,
hasMore, hasMore,
onLoadMore, onLoadMore,
renderItem, renderItem,
title,
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",
debugInfo,
}: InfiniteScrollListProps<T>) => { }: InfiniteScrollListProps<T>) => {
const observerRef = useRef<HTMLDivElement>(null); const observerRef = useRef<HTMLDivElement>(null);
@ -59,22 +53,16 @@ const InfiniteScrollList = <T extends { key?: string }>({
return () => observer.disconnect(); return () => observer.disconnect();
}, [onLoadMore, hasMore, isLoading, items.length]); }, [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 ( return (
<div className="max-w-4xl mx-auto p-6"> <div className="max-w-4xl mx-auto p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 mb-2">{title}</h1>
{subtitle && (
<p className="text-sm text-gray-600">{subtitle}</p>
)}
{/* Debug info */}
{debugInfo && (
<div className="mt-2 text-sm text-gray-500">
{debugInfo}
</div>
)}
</div>
{/* List */} {/* List */}
<div className="bg-white rounded-lg shadow"> <div className="bg-white rounded-lg shadow">
{items.length === 0 && !isLoading ? ( {items.length === 0 && !isLoading ? (
@ -98,9 +86,9 @@ const InfiniteScrollList = <T extends { key?: string }>({
} }
/> />
) : ( ) : (
<div className="divide-y divide-gray-200"> <div>
{items.map((item, index) => ( {items.map((item, index) => (
<div key={item.key}> <div key={getItemKey(item, index)} className="px-4">
{renderItem(item, index)} {renderItem(item, index)}
</div> </div>
))} ))}

View File

@ -0,0 +1,21 @@
import React from 'react';
interface PageHeaderProps {
title: string;
subtitle?: string;
}
const PageHeader: React.FC<PageHeaderProps> = ({ title, subtitle }) => {
return (
<div className="max-w-4xl mx-auto p-6">
<div style={{ marginBottom: '24px', paddingLeft: '16px' }}>
<h1 className="text-2xl font-bold text-gray-900 mb-2">{title}</h1>
{subtitle && (
<p className="text-sm text-gray-600">{subtitle}</p>
)}
</div>
</div>
);
};
export default PageHeader;

View File

@ -1,6 +1,9 @@
import React from 'react'; 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 ActionButton from './ActionButton';
import { useAppSelector } from '../../redux';
import { selectQueue, selectFavorites } from '../../redux';
import type { SongItemProps } from '../../types'; import type { SongItemProps } from '../../types';
// Utility function to extract filename from path // Utility function to extract filename from path
@ -23,127 +26,91 @@ const SongItem: React.FC<SongItemProps> = ({
isAdmin = false, isAdmin = false,
className = '' 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 = () => { const renderActionPanel = () => {
switch (context) { const buttons = [];
case 'search':
return ( // Add to Queue button (for all contexts except queue, only if not already in queue)
<div className="flex gap-2"> if (context !== 'queue' && !isInQueue) {
<ActionButton buttons.push(
onClick={onAddToQueue || (() => {})} <ActionButton
variant="primary" key="add"
size="sm" onClick={onAddToQueue || (() => {})}
> variant="primary"
Add to Queue size="sm"
</ActionButton> >
<ActionButton <IonIcon icon={add} />
onClick={onToggleFavorite || (() => {})} </ActionButton>
variant={song.favorite ? 'danger' : 'secondary'} );
size="sm"
>
{song.favorite ? '❤️' : '🤍'}
</ActionButton>
</div>
);
case 'queue':
return (
<div className="flex gap-2">
{isAdmin && onRemoveFromQueue && (
<ActionButton
onClick={onRemoveFromQueue}
variant="danger"
size="sm"
>
Remove
</ActionButton>
)}
<ActionButton
onClick={onToggleFavorite || (() => {})}
variant={song.favorite ? 'danger' : 'secondary'}
size="sm"
>
{song.favorite ? '❤️' : '🤍'}
</ActionButton>
</div>
);
case 'history':
return (
<div className="flex gap-2">
<ActionButton
onClick={onAddToQueue || (() => {})}
variant="primary"
size="sm"
>
Add to Queue
</ActionButton>
<ActionButton
onClick={onToggleFavorite || (() => {})}
variant={song.favorite ? 'danger' : 'secondary'}
size="sm"
>
{song.favorite ? '❤️' : '🤍'}
</ActionButton>
</div>
);
case 'favorites':
return (
<div className="flex gap-2">
<ActionButton
onClick={onAddToQueue || (() => {})}
variant="primary"
size="sm"
>
Add to Queue
</ActionButton>
<ActionButton
onClick={onDelete || (() => {})}
variant="danger"
size="sm"
>
Remove
</ActionButton>
</div>
);
case 'topPlayed':
return (
<div className="flex gap-2">
<ActionButton
onClick={onAddToQueue || (() => {})}
variant="primary"
size="sm"
>
Add to Queue
</ActionButton>
<ActionButton
onClick={onToggleFavorite || (() => {})}
variant={song.favorite ? 'danger' : 'secondary'}
size="sm"
>
{song.favorite ? '❤️' : '🤍'}
</ActionButton>
</div>
);
default:
return null;
} }
// Remove from Queue button (only for queue context, admin only)
if (context === 'queue' && isAdmin && onRemoveFromQueue) {
buttons.push(
<ActionButton
key="remove"
onClick={onRemoveFromQueue}
variant="danger"
size="sm"
>
<IonIcon icon={trash} />
</ActionButton>
);
}
// Delete from Favorites button (only for favorites context)
if (context === 'favorites' && onDelete) {
buttons.push(
<ActionButton
key="delete"
onClick={onDelete}
variant="danger"
size="sm"
>
<IonIcon icon={trash} />
</ActionButton>
);
}
// Toggle Favorite button (for all contexts except favorites)
if (context !== 'favorites') {
buttons.push(
<ActionButton
key="favorite"
onClick={onToggleFavorite || (() => {})}
variant={isInFavorites ? 'danger' : 'secondary'}
size="sm"
>
<IonIcon icon={isInFavorites ? heart : heartOutline} />
</ActionButton>
);
}
return buttons.length > 0 ? (
<div className="flex gap-2">
{buttons}
</div>
) : null;
}; };
return ( return (
<IonItem className={className}> <IonItem className={className}>
<IonLabel> <IonLabel className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-gray-900 truncate"> <h3 className="text-base font-extrabold text-gray-900 break-words">
{song.title} {song.title}
</h3> </h3>
<p className="text-sm text-gray-500 truncate"> <p className="text-sm italic text-gray-500 break-words">
{song.artist} {song.artist}
</p> </p>
{/* Show filename for all contexts except queue */} {/* Show filename for all contexts except queue */}
{context !== 'queue' && song.path && ( {context !== 'queue' && song.path && (
<p className="text-xs text-gray-400 truncate"> <p className="text-xs text-gray-400 break-words">
{extractFilename(song.path)} {extractFilename(song.path)}
</p> </p>
)} )}
@ -154,7 +121,7 @@ const SongItem: React.FC<SongItemProps> = ({
)} )}
</IonLabel> </IonLabel>
<div slot="end" className="flex gap-2"> <div slot="end" className="flex gap-2 flex-shrink-0 ml-2">
{renderActionPanel()} {renderActionPanel()}
</div> </div>
</IonItem> </IonItem>

View File

@ -3,5 +3,6 @@ export { default as EmptyState } from './EmptyState';
export { default as Toast } from './Toast'; export { default as Toast } from './Toast';
export { default as ErrorBoundary } from './ErrorBoundary'; export { default as ErrorBoundary } from './ErrorBoundary';
export { default as InfiniteScrollList } from './InfiniteScrollList'; export { default as InfiniteScrollList } from './InfiniteScrollList';
export { default as PageHeader } from './PageHeader';
export { default as SongItem } from './SongItem'; export { default as SongItem } from './SongItem';
export { default as PlayerControls } from './PlayerControls'; export { default as PlayerControls } from './PlayerControls';

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState } from 'react';
import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonChip } from '@ionic/react'; import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon } from '@ionic/react';
import { close, add, heart, heartOutline } from 'ionicons/icons'; import { close, add, heart, heartOutline, list } from 'ionicons/icons';
import { InfiniteScrollList, PageHeader } from '../../components/common';
import { useArtists } from '../../hooks'; import { useArtists } from '../../hooks';
import { useAppSelector } from '../../redux'; import { useAppSelector } from '../../redux';
import { selectSongs } from '../../redux'; import { selectSongs } from '../../redux';
@ -8,7 +9,6 @@ import { selectSongs } from '../../redux';
const Artists: React.FC = () => { const Artists: React.FC = () => {
const { const {
artists, artists,
allArtists,
searchTerm, searchTerm,
hasMore, hasMore,
loadMore, loadMore,
@ -22,40 +22,12 @@ const Artists: React.FC = () => {
const songs = useAppSelector(selectSongs); const songs = useAppSelector(selectSongs);
const songsCount = Object.keys(songs).length; const songsCount = Object.keys(songs).length;
const [selectedArtist, setSelectedArtist] = useState<string | null>(null); const [selectedArtist, setSelectedArtist] = useState<string | null>(null);
const observerRef = useRef<HTMLDivElement>(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 // Debug logging
useEffect(() => { console.log('Artists component - artists count:', artists.length);
console.log('Artists component - artists count:', artists.length); console.log('Artists component - selected artist:', selectedArtist);
console.log('Artists component - selected artist:', selectedArtist); console.log('Artists component - songs count:', songsCount);
}, [artists.length, selectedArtist]); console.log('Artists component - search term:', searchTerm);
const handleArtistClick = (artist: string) => { const handleArtistClick = (artist: string) => {
setSelectedArtist(artist); setSelectedArtist(artist);
@ -67,6 +39,21 @@ const Artists: React.FC = () => {
const selectedArtistSongs = selectedArtist ? getSongsByArtist(selectedArtist) : []; const selectedArtistSongs = selectedArtist ? getSongsByArtist(selectedArtist) : [];
// Render artist item for InfiniteScrollList
const renderArtistItem = (artist: string) => (
<IonItem button onClick={() => handleArtistClick(artist)} detail={false}>
<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>
<IonIcon icon={list} slot="end" color="primary" />
</IonItem>
);
return ( return (
<> <>
<IonHeader> <IonHeader>
@ -74,10 +61,14 @@ const Artists: React.FC = () => {
<IonTitle>Artists</IonTitle> <IonTitle>Artists</IonTitle>
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<PageHeader
title="Artists"
subtitle="Browse songs by artist"
/>
<div className="max-w-4xl mx-auto p-6"> <div className="max-w-4xl mx-auto p-6">
<div className="mb-6"> <div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Artists</h1>
{/* Search Input */} {/* Search Input */}
<IonSearchbar <IonSearchbar
placeholder="Search artists..." placeholder="Search artists..."
@ -86,75 +77,20 @@ const Artists: React.FC = () => {
debounce={300} debounce={300}
showClearButton="focus" showClearButton="focus"
/> />
{/* Debug info */}
<div className="mt-2 text-sm text-gray-500">
Total songs loaded: {songsCount} | Showing: {artists.length} of {allArtists.length} artists | Search term: "{searchTerm}"
</div>
</div> </div>
{/* Artists List */} {/* Artists List */}
<div className="bg-white rounded-lg shadow"> <InfiniteScrollList<string>
{songsCount === 0 ? ( items={artists}
<div className="p-8 text-center"> isLoading={songsCount === 0}
<div className="text-gray-400 mb-4"> hasMore={hasMore}
<svg className="h-12 w-12 mx-auto animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"> onLoadMore={loadMore}
<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" /> renderItem={renderArtistItem}
</svg> emptyTitle={searchTerm ? "No artists found" : "No artists available"}
</div> emptyMessage={searchTerm ? "Try adjusting your search terms" : "Artists will appear here once songs are loaded"}
<h3 className="text-lg font-medium text-gray-900 mb-2">Loading artists...</h3> loadingTitle="Loading artists..."
<p className="text-sm text-gray-500">Please wait while songs are being loaded from the database</p> loadingMessage="Please wait while songs are being loaded from the database"
</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>
)}
</IonList>
)}
</div>
{/* Artist Songs Modal */} {/* Artist Songs Modal */}
<IonModal isOpen={!!selectedArtist} onDidDismiss={handleCloseArtistSongs}> <IonModal isOpen={!!selectedArtist} onDidDismiss={handleCloseArtistSongs}>

View File

@ -1,6 +1,6 @@
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, SongItem } from '../../components/common'; import { InfiniteScrollList, PageHeader, 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';
@ -35,6 +35,11 @@ const Favorites: React.FC = () => {
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<PageHeader
title="Favorites"
subtitle={`${favoritesCount} items loaded`}
/>
<InfiniteScrollList<Song> <InfiniteScrollList<Song>
items={favoritesItems} items={favoritesItems}
isLoading={favoritesCount === 0} isLoading={favoritesCount === 0}
@ -48,13 +53,10 @@ const Favorites: React.FC = () => {
onToggleFavorite={() => handleToggleFavorite(song)} 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..."
loadingMessage="Please wait while favorites data is being loaded" loadingMessage="Please wait while favorites data is being loaded"
debugInfo={`Favorites items loaded: ${favoritesCount}`}
/> />
</> </>
); );

View File

@ -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, SongItem } from '../../components/common'; import { InfiniteScrollList, PageHeader, 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';
@ -50,6 +50,11 @@ const History: React.FC = () => {
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<PageHeader
title="Recently Played"
subtitle={`${historyCount} items loaded`}
/>
<div style={{ height: '100%', overflowY: 'auto' }}> <div style={{ height: '100%', overflowY: 'auto' }}>
<InfiniteScrollList<Song> <InfiniteScrollList<Song>
items={historyItems} items={historyItems}
@ -69,13 +74,10 @@ const History: React.FC = () => {
{renderExtraContent(song)} {renderExtraContent(song)}
</div> </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}`}
/> />
</div> </div>
</> </>

View File

@ -1,6 +1,6 @@
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, SongItem } from '../../components/common'; import { InfiniteScrollList, PageHeader, 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';
@ -35,6 +35,11 @@ const NewSongs: React.FC = () => {
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<PageHeader
title="New Songs"
subtitle={`${newSongsCount} items loaded`}
/>
<InfiniteScrollList<Song> <InfiniteScrollList<Song>
items={newSongsItems} items={newSongsItems}
isLoading={newSongsCount === 0} isLoading={newSongsCount === 0}
@ -48,13 +53,10 @@ const NewSongs: React.FC = () => {
onToggleFavorite={() => handleToggleFavorite(song)} 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..."
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}`}
/> />
</> </>
); );

View File

@ -74,7 +74,7 @@ const Queue: React.FC = () => {
} }
/> />
) : ( ) : (
<IonList> <IonList className="px-4">
{queueItems.map((queueItem, index) => { {queueItems.map((queueItem, index) => {
console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`); console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`);
const canDelete = index === 0 ? canDeleteFirstItem : true; const canDelete = index === 0 ? canDeleteFirstItem : true;

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { IonSearchbar } from '@ionic/react'; import { IonSearchbar } from '@ionic/react';
import { InfiniteScrollList, SongItem } from '../../components/common'; import { InfiniteScrollList, PageHeader, 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';
@ -37,9 +37,12 @@ const Search: React.FC = () => {
return ( return (
<div className="max-w-4xl mx-auto p-6"> <div className="max-w-4xl mx-auto p-6">
<PageHeader
title="Search Songs"
subtitle="Search by title or artist"
/>
<div className="mb-6"> <div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 mb-4">Search Songs</h1>
{/* Search Input */} {/* Search Input */}
<IonSearchbar <IonSearchbar
placeholder="Search by title or artist..." placeholder="Search by title or artist..."
@ -70,12 +73,10 @@ const Search: React.FC = () => {
isAdmin={isAdmin} isAdmin={isAdmin}
/> />
)} )}
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"
debugInfo=""
/> />
{/* Search Stats */} {/* Search Stats */}

View File

@ -3,7 +3,7 @@ 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 { InfiniteScrollList, PageHeader } from '../../components/common';
import type { TopPlayed } from '../../types'; import type { TopPlayed } from '../../types';
const Top100: React.FC = () => { const Top100: React.FC = () => {
@ -110,6 +110,11 @@ const Top100: React.FC = () => {
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<PageHeader
title="Top 100 Played"
subtitle={`${displayCount} items loaded (Mock Data)`}
/>
<InfiniteScrollList<TopPlayed> <InfiniteScrollList<TopPlayed>
items={displayItems} items={displayItems}
isLoading={false} isLoading={false}
@ -133,11 +138,8 @@ const Top100: React.FC = () => {
</div> </div>
</div> </div>
)} )}
title="Top 100 Played"
subtitle={`${displayCount} items loaded (Mock Data)`}
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"
debugInfo={`Top played items loaded: ${displayCount} (Mock Data)`}
/> />
</> </>
); );