Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
2d5f8fdb8f
commit
97c1b1e030
@ -1,15 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IonApp, IonHeader, IonToolbar, IonTitle, IonContent, IonChip, IonMenuButton } from '@ionic/react';
|
||||
import { selectCurrentSinger, selectIsAdmin, selectControllerName } from '../../redux/authSlice';
|
||||
import { IonApp, IonHeader, IonToolbar, IonTitle, IonContent, IonMenuButton, IonIcon } from '@ionic/react';
|
||||
import { logOut } from 'ionicons/icons';
|
||||
import { selectControllerName } from '../../redux/authSlice';
|
||||
import { logout } from '../../redux/authSlice';
|
||||
import { ActionButton } from '../common';
|
||||
import Navigation from '../Navigation/Navigation';
|
||||
import type { LayoutProps } from '../../types';
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
const currentSinger = useSelector(selectCurrentSinger);
|
||||
const isAdmin = useSelector(selectIsAdmin);
|
||||
const controllerName = useSelector(selectControllerName);
|
||||
const dispatch = useDispatch();
|
||||
const [isLargeScreen, setIsLargeScreen] = useState(false);
|
||||
@ -52,35 +51,25 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
|
||||
<IonTitle>
|
||||
<div className="flex items-center">
|
||||
<span>🎤 Karaoke App</span>
|
||||
<span>Sings A Lot</span>
|
||||
{controllerName && (
|
||||
<span className="ml-4 text-sm text-gray-500">
|
||||
Party: {controllerName}
|
||||
: {controllerName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</IonTitle>
|
||||
|
||||
{/* User Info & Logout */}
|
||||
{currentSinger && (
|
||||
<div slot="end" className="flex items-center space-x-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">{currentSinger}</span>
|
||||
{isAdmin && (
|
||||
<IonChip color="primary">
|
||||
Admin
|
||||
</IonChip>
|
||||
)}
|
||||
</div>
|
||||
<ActionButton
|
||||
onClick={handleLogout}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Logout
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
{/* Logout Button */}
|
||||
<div slot="end">
|
||||
<ActionButton
|
||||
onClick={handleLogout}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
<IonIcon icon={logOut} />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
|
||||
@ -7,28 +7,22 @@ interface InfiniteScrollListProps<T> {
|
||||
hasMore: boolean;
|
||||
onLoadMore: () => void;
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
emptyTitle: string;
|
||||
emptyMessage: string;
|
||||
loadingTitle?: string;
|
||||
loadingMessage?: string;
|
||||
debugInfo?: string;
|
||||
}
|
||||
|
||||
const InfiniteScrollList = <T extends { key?: string }>({
|
||||
const InfiniteScrollList = <T extends string | { key?: string }>({
|
||||
items,
|
||||
isLoading,
|
||||
hasMore,
|
||||
onLoadMore,
|
||||
renderItem,
|
||||
title,
|
||||
subtitle,
|
||||
emptyTitle,
|
||||
emptyMessage,
|
||||
loadingTitle = "Loading...",
|
||||
loadingMessage = "Please wait while data is being loaded",
|
||||
debugInfo,
|
||||
}: InfiniteScrollListProps<T>) => {
|
||||
const observerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -59,22 +53,16 @@ const InfiniteScrollList = <T extends { key?: string }>({
|
||||
return () => observer.disconnect();
|
||||
}, [onLoadMore, hasMore, isLoading, items.length]);
|
||||
|
||||
// Generate key for item
|
||||
const getItemKey = (item: T, index: number): string => {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
return item.key || `item-${index}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
{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) => (
|
||||
<div key={item.key}>
|
||||
<div key={getItemKey(item, index)} className="px-4">
|
||||
{renderItem(item, index)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
21
src/components/common/PageHeader.tsx
Normal file
21
src/components/common/PageHeader.tsx
Normal 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;
|
||||
@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import { IonItem, IonLabel } from '@ionic/react';
|
||||
import { IonItem, IonLabel, IonIcon } from '@ionic/react';
|
||||
import { add, heart, heartOutline, trash } from 'ionicons/icons';
|
||||
import ActionButton from './ActionButton';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectQueue, selectFavorites } from '../../redux';
|
||||
import type { SongItemProps } from '../../types';
|
||||
|
||||
// Utility function to extract filename from path
|
||||
@ -23,127 +26,91 @@ const SongItem: React.FC<SongItemProps> = ({
|
||||
isAdmin = false,
|
||||
className = ''
|
||||
}) => {
|
||||
// Get current state from Redux
|
||||
const queue = useAppSelector(selectQueue);
|
||||
const favorites = useAppSelector(selectFavorites);
|
||||
|
||||
// Check if song is in queue or favorites based on path
|
||||
const isInQueue = Object.values(queue).some(item => item.song.path === song.path);
|
||||
const isInFavorites = Object.values(favorites).some(favSong => favSong.path === song.path);
|
||||
const renderActionPanel = () => {
|
||||
switch (context) {
|
||||
case 'search':
|
||||
return (
|
||||
<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 '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;
|
||||
const buttons = [];
|
||||
|
||||
// Add to Queue button (for all contexts except queue, only if not already in queue)
|
||||
if (context !== 'queue' && !isInQueue) {
|
||||
buttons.push(
|
||||
<ActionButton
|
||||
key="add"
|
||||
onClick={onAddToQueue || (() => {})}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
<IonIcon icon={add} />
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<IonItem className={className}>
|
||||
<IonLabel>
|
||||
<h3 className="text-sm font-medium text-gray-900 truncate">
|
||||
<IonLabel className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-extrabold text-gray-900 break-words">
|
||||
{song.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
<p className="text-sm italic text-gray-500 break-words">
|
||||
{song.artist}
|
||||
</p>
|
||||
{/* Show filename for all contexts except queue */}
|
||||
{context !== 'queue' && song.path && (
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
<p className="text-xs text-gray-400 break-words">
|
||||
{extractFilename(song.path)}
|
||||
</p>
|
||||
)}
|
||||
@ -154,7 +121,7 @@ const SongItem: React.FC<SongItemProps> = ({
|
||||
)}
|
||||
</IonLabel>
|
||||
|
||||
<div slot="end" className="flex gap-2">
|
||||
<div slot="end" className="flex gap-2 flex-shrink-0 ml-2">
|
||||
{renderActionPanel()}
|
||||
</div>
|
||||
</IonItem>
|
||||
|
||||
@ -3,5 +3,6 @@ export { default as EmptyState } from './EmptyState';
|
||||
export { default as Toast } from './Toast';
|
||||
export { default as ErrorBoundary } from './ErrorBoundary';
|
||||
export { default as InfiniteScrollList } from './InfiniteScrollList';
|
||||
export { default as PageHeader } from './PageHeader';
|
||||
export { default as SongItem } from './SongItem';
|
||||
export { default as PlayerControls } from './PlayerControls';
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonChip } from '@ionic/react';
|
||||
import { close, add, heart, heartOutline } from 'ionicons/icons';
|
||||
import React, { useState } from 'react';
|
||||
import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon } from '@ionic/react';
|
||||
import { close, add, heart, heartOutline, list } from 'ionicons/icons';
|
||||
import { InfiniteScrollList, PageHeader } from '../../components/common';
|
||||
import { useArtists } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectSongs } from '../../redux';
|
||||
@ -8,7 +9,6 @@ import { selectSongs } from '../../redux';
|
||||
const Artists: React.FC = () => {
|
||||
const {
|
||||
artists,
|
||||
allArtists,
|
||||
searchTerm,
|
||||
hasMore,
|
||||
loadMore,
|
||||
@ -22,40 +22,12 @@ const Artists: React.FC = () => {
|
||||
const songs = useAppSelector(selectSongs);
|
||||
const songsCount = Object.keys(songs).length;
|
||||
const [selectedArtist, setSelectedArtist] = useState<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
|
||||
useEffect(() => {
|
||||
console.log('Artists component - artists count:', artists.length);
|
||||
console.log('Artists component - selected artist:', selectedArtist);
|
||||
}, [artists.length, selectedArtist]);
|
||||
console.log('Artists component - artists count:', artists.length);
|
||||
console.log('Artists component - selected artist:', selectedArtist);
|
||||
console.log('Artists component - songs count:', songsCount);
|
||||
console.log('Artists component - search term:', searchTerm);
|
||||
|
||||
const handleArtistClick = (artist: string) => {
|
||||
setSelectedArtist(artist);
|
||||
@ -67,6 +39,21 @@ const Artists: React.FC = () => {
|
||||
|
||||
const selectedArtistSongs = selectedArtist ? getSongsByArtist(selectedArtist) : [];
|
||||
|
||||
// Render artist item for InfiniteScrollList
|
||||
const renderArtistItem = (artist: string) => (
|
||||
<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 (
|
||||
<>
|
||||
<IonHeader>
|
||||
@ -74,10 +61,14 @@ const Artists: React.FC = () => {
|
||||
<IonTitle>Artists</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<PageHeader
|
||||
title="Artists"
|
||||
subtitle="Browse songs by artist"
|
||||
/>
|
||||
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Artists</h1>
|
||||
|
||||
{/* Search Input */}
|
||||
<IonSearchbar
|
||||
placeholder="Search artists..."
|
||||
@ -86,75 +77,20 @@ const Artists: React.FC = () => {
|
||||
debounce={300}
|
||||
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>
|
||||
|
||||
{/* Artists List */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
{songsCount === 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 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>
|
||||
)}
|
||||
</IonList>
|
||||
)}
|
||||
</div>
|
||||
<InfiniteScrollList<string>
|
||||
items={artists}
|
||||
isLoading={songsCount === 0}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
renderItem={renderArtistItem}
|
||||
emptyTitle={searchTerm ? "No artists found" : "No artists available"}
|
||||
emptyMessage={searchTerm ? "Try adjusting your search terms" : "Artists will appear here once songs are loaded"}
|
||||
loadingTitle="Loading artists..."
|
||||
loadingMessage="Please wait while songs are being loaded from the database"
|
||||
/>
|
||||
|
||||
{/* Artist Songs Modal */}
|
||||
<IonModal isOpen={!!selectedArtist} onDidDismiss={handleCloseArtistSongs}>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
|
||||
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||
import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common';
|
||||
import { useFavorites } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectFavorites } from '../../redux';
|
||||
@ -35,6 +35,11 @@ const Favorites: React.FC = () => {
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<PageHeader
|
||||
title="Favorites"
|
||||
subtitle={`${favoritesCount} items loaded`}
|
||||
/>
|
||||
|
||||
<InfiniteScrollList<Song>
|
||||
items={favoritesItems}
|
||||
isLoading={favoritesCount === 0}
|
||||
@ -48,13 +53,10 @@ const Favorites: React.FC = () => {
|
||||
onToggleFavorite={() => handleToggleFavorite(song)}
|
||||
/>
|
||||
)}
|
||||
title="Favorites"
|
||||
subtitle={`${favoritesCount} items loaded`}
|
||||
emptyTitle="No favorites yet"
|
||||
emptyMessage="Add songs to your favorites to see them here"
|
||||
loadingTitle="Loading favorites..."
|
||||
loadingMessage="Please wait while favorites data is being loaded"
|
||||
debugInfo={`Favorites items loaded: ${favoritesCount}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { IonHeader, IonToolbar, IonTitle, IonChip, IonIcon } from '@ionic/react';
|
||||
import { time } from 'ionicons/icons';
|
||||
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||
import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common';
|
||||
import { useHistory } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectHistory } from '../../redux';
|
||||
@ -50,6 +50,11 @@ const History: React.FC = () => {
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<PageHeader
|
||||
title="Recently Played"
|
||||
subtitle={`${historyCount} items loaded`}
|
||||
/>
|
||||
|
||||
<div style={{ height: '100%', overflowY: 'auto' }}>
|
||||
<InfiniteScrollList<Song>
|
||||
items={historyItems}
|
||||
@ -69,13 +74,10 @@ const History: React.FC = () => {
|
||||
{renderExtraContent(song)}
|
||||
</div>
|
||||
)}
|
||||
title="Recently Played"
|
||||
subtitle={`${historyCount} items loaded`}
|
||||
emptyTitle="No history yet"
|
||||
emptyMessage="Songs will appear here after they've been played"
|
||||
loadingTitle="Loading history..."
|
||||
loadingMessage="Please wait while history data is being loaded"
|
||||
debugInfo={`History items loaded: ${historyCount}`}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
|
||||
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||
import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common';
|
||||
import { useNewSongs } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectNewSongs } from '../../redux';
|
||||
@ -35,6 +35,11 @@ const NewSongs: React.FC = () => {
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<PageHeader
|
||||
title="New Songs"
|
||||
subtitle={`${newSongsCount} items loaded`}
|
||||
/>
|
||||
|
||||
<InfiniteScrollList<Song>
|
||||
items={newSongsItems}
|
||||
isLoading={newSongsCount === 0}
|
||||
@ -48,13 +53,10 @@ const NewSongs: React.FC = () => {
|
||||
onToggleFavorite={() => handleToggleFavorite(song)}
|
||||
/>
|
||||
)}
|
||||
title="New Songs"
|
||||
subtitle={`${newSongsCount} items loaded`}
|
||||
emptyTitle="No new songs"
|
||||
emptyMessage="Check back later for new additions"
|
||||
loadingTitle="Loading new songs..."
|
||||
loadingMessage="Please wait while new songs data is being loaded"
|
||||
debugInfo={`New songs loaded: ${newSongsCount}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -74,7 +74,7 @@ const Queue: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<IonList>
|
||||
<IonList className="px-4">
|
||||
{queueItems.map((queueItem, index) => {
|
||||
console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`);
|
||||
const canDelete = index === 0 ? canDeleteFirstItem : true;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IonSearchbar } from '@ionic/react';
|
||||
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||
import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common';
|
||||
import { useSearch } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectIsAdmin, selectSongs } from '../../redux';
|
||||
@ -37,9 +37,12 @@ const Search: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<PageHeader
|
||||
title="Search Songs"
|
||||
subtitle="Search by title or artist"
|
||||
/>
|
||||
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Search Songs</h1>
|
||||
|
||||
{/* Search Input */}
|
||||
<IonSearchbar
|
||||
placeholder="Search by title or artist..."
|
||||
@ -70,12 +73,10 @@ const Search: React.FC = () => {
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
)}
|
||||
title=""
|
||||
emptyTitle={searchTerm ? "No songs found" : "No songs available"}
|
||||
emptyMessage={searchTerm ? "Try adjusting your search terms" : "Songs will appear here once loaded"}
|
||||
loadingTitle="Loading songs..."
|
||||
loadingMessage="Please wait while songs are being loaded from the database"
|
||||
debugInfo=""
|
||||
/>
|
||||
|
||||
{/* Search Stats */}
|
||||
|
||||
@ -3,7 +3,7 @@ import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
|
||||
import { useTopPlayed } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectTopPlayed } from '../../redux';
|
||||
import { InfiniteScrollList } from '../../components/common';
|
||||
import { InfiniteScrollList, PageHeader } from '../../components/common';
|
||||
import type { TopPlayed } from '../../types';
|
||||
|
||||
const Top100: React.FC = () => {
|
||||
@ -110,6 +110,11 @@ const Top100: React.FC = () => {
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<PageHeader
|
||||
title="Top 100 Played"
|
||||
subtitle={`${displayCount} items loaded (Mock Data)`}
|
||||
/>
|
||||
|
||||
<InfiniteScrollList<TopPlayed>
|
||||
items={displayItems}
|
||||
isLoading={false}
|
||||
@ -133,11 +138,8 @@ const Top100: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
title="Top 100 Played"
|
||||
subtitle={`${displayCount} items loaded (Mock Data)`}
|
||||
emptyTitle="No top played songs"
|
||||
emptyMessage="Play some songs to see the top played list"
|
||||
debugInfo={`Top played items loaded: ${displayCount} (Mock Data)`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user