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

This commit is contained in:
Matt Bruce 2025-07-18 09:25:55 -05:00
parent 97c1b1e030
commit 3f70aed96a
10 changed files with 272 additions and 391 deletions

View File

@ -40,3 +40,37 @@
.read-the-docs {
color: #888;
}
/* Override Ionic accordion separator styling with higher specificity */
ion-accordion.accordion-no-full-separator {
--border-style: none !important;
}
ion-accordion.accordion-no-full-separator::part(header) {
--border-style: none !important;
}
ion-accordion.accordion-no-full-separator ion-item.item-no-full-separator {
--border-style: solid !important;
--border-width: 0 0 1px 0 !important;
--border-color: var(--ion-item-border-color, rgba(0, 0, 0, 0.13)) !important;
}
ion-accordion.accordion-no-full-separator ion-item.item-no-full-separator::part(native) {
--border-style: solid !important;
--border-width: 0 0 1px 0 !important;
--border-color: var(--ion-item-border-color, rgba(0, 0, 0, 0.13)) !important;
}
/* Alternative approach: Target all accordion items globally */
ion-accordion ion-item {
--border-style: solid !important;
--border-width: 0 0 1px 0 !important;
--border-color: var(--ion-item-border-color, rgba(0, 0, 0, 0.13)) !important;
}
ion-accordion ion-item::part(native) {
--border-style: solid !important;
--border-width: 0 0 1px 0 !important;
--border-color: var(--ion-item-border-color, rgba(0, 0, 0, 0.13)) !important;
}

View File

@ -114,7 +114,7 @@ const InfiniteScrollList = <T extends string | { key?: string }>({
{/* Stats */}
{items.length > 0 && (
<div className="mt-4 text-sm text-gray-500 text-center">
<div style={{ marginTop: '16px', marginBottom: '24px' }} className="text-sm text-gray-500 text-center">
Showing {items.length} item{items.length !== 1 ? 's' : ''}
{hasMore && ` • Scroll down to load more`}
</div>

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon } from '@ionic/react';
import { IonSearchbar, IonList, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent } from '@ionic/react';
import { close, add, heart, heartOutline, list } from 'ionicons/icons';
import { InfiniteScrollList, PageHeader } from '../../components/common';
import { useArtists } from '../../hooks';
@ -56,15 +56,8 @@ const Artists: React.FC = () => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>Artists</IonTitle>
</IonToolbar>
</IonHeader>
<PageHeader
title="Artists"
subtitle="Browse songs by artist"
/>
<div className="max-w-4xl mx-auto p-6">
@ -103,7 +96,7 @@ const Artists: React.FC = () => {
</IonToolbar>
</IonHeader>
<div className="p-4">
<IonContent>
<IonList>
{selectedArtistSongs.map((song) => (
<IonItem key={song.key}>
@ -134,7 +127,7 @@ const Artists: React.FC = () => {
</IonItem>
))}
</IonList>
</div>
</IonContent>
</IonModal>
</div>
</>

View File

@ -1,5 +1,4 @@
import React from 'react';
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common';
import { useFavorites } from '../../hooks';
import { useAppSelector } from '../../redux';
@ -24,17 +23,6 @@ const Favorites: React.FC = () => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>
Favorites
<IonChip color="primary" className="ml-2">
{favoritesItems.length}
</IonChip>
</IonTitle>
</IonToolbar>
</IonHeader>
<PageHeader
title="Favorites"
subtitle={`${favoritesCount} items loaded`}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { IonHeader, IonToolbar, IonTitle, IonChip, IonIcon } from '@ionic/react';
import { IonChip, IonIcon } from '@ionic/react';
import { time } from 'ionicons/icons';
import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common';
import { useHistory } from '../../hooks';
@ -39,17 +39,6 @@ const History: React.FC = () => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>
Recently Played
<IonChip color="primary" className="ml-2">
{historyItems.length}
</IonChip>
</IonTitle>
</IonToolbar>
</IonHeader>
<PageHeader
title="Recently Played"
subtitle={`${historyCount} items loaded`}

View File

@ -1,5 +1,4 @@
import React from 'react';
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common';
import { useNewSongs } from '../../hooks';
import { useAppSelector } from '../../redux';
@ -24,17 +23,6 @@ const NewSongs: React.FC = () => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>
New Songs
<IonChip color="primary" className="ml-2">
{newSongsItems.length}
</IonChip>
</IonTitle>
</IonToolbar>
</IonHeader>
<PageHeader
title="New Songs"
subtitle={`${newSongsCount} items loaded`}

View File

@ -1,11 +1,12 @@
import React from 'react';
import { IonList, IonItem, IonItemSliding, IonItemOptions, IonItemOption, IonIcon, IonLabel, IonChip } from '@ionic/react';
import { IonItem, IonItemSliding, IonItemOptions, IonItemOption, IonIcon, IonLabel, IonChip } from '@ionic/react';
import { trash, arrowUp, arrowDown } from 'ionicons/icons';
import { EmptyState, ActionButton, PlayerControls } from '../../components/common';
import { ActionButton, PlayerControls, InfiniteScrollList, PageHeader } from '../../components/common';
import { useQueue } from '../../hooks';
import { useAppSelector } from '../../redux';
import { selectQueue, selectPlayerState } from '../../redux';
import { PlayerState } from '../../types';
import type { QueueItem } from '../../types';
const Queue: React.FC = () => {
const {
@ -32,124 +33,101 @@ const Queue: React.FC = () => {
console.log('Queue component - canDeleteFirstItem:', canDeleteFirstItem);
console.log('Queue component - canReorder:', canReorder);
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">Queue</h1>
<p className="text-sm text-gray-600">
{queueStats.totalSongs} song{queueStats.totalSongs !== 1 ? 's' : ''} in queue
</p>
{/* Debug info */}
<div className="mt-2 text-sm text-gray-500">
Queue items loaded: {queueCount}
</div>
</div>
// Render queue item for InfiniteScrollList
const renderQueueItem = (queueItem: QueueItem, index: number) => {
console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`);
const canDelete = index === 0 ? canDeleteFirstItem : true;
return (
<IonItemSliding key={queueItem.key}>
<IonItem>
{/* Order Number */}
<div slot="start" className="flex-shrink-0 w-12 h-12 flex items-center justify-center bg-gray-100 text-gray-600 font-medium rounded-full">
{queueItem.order}
</div>
{/* Player Controls - Only visible to admin users */}
<div className="mb-6">
<PlayerControls />
</div>
{/* Song Info */}
<IonLabel>
<h3 className="text-sm font-medium text-gray-900 truncate">
{queueItem.song.title}
</h3>
<p className="text-sm text-gray-500 truncate">
{queueItem.song.artist}
</p>
</IonLabel>
{/* Queue List */}
<div className="bg-white rounded-lg shadow">
{queueCount === 0 ? (
<EmptyState
title="Queue is empty"
message="Add songs from search, history, or favorites to get started"
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="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>
}
/>
) : queueItems.length === 0 ? (
<EmptyState
title="Loading queue..."
message="Please wait while queue 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 className="px-4">
{queueItems.map((queueItem, index) => {
console.log(`Queue item ${index}: order=${queueItem.order}, key=${queueItem.key}`);
const canDelete = index === 0 ? canDeleteFirstItem : true;
return (
<IonItemSliding key={queueItem.key}>
<IonItem>
{/* Order Number */}
<div slot="start" className="flex-shrink-0 w-12 h-12 flex items-center justify-center bg-gray-100 text-gray-600 font-medium rounded-full">
{queueItem.order}
</div>
{/* Singer Pill - Moved to far right */}
<IonChip slot="end" color="medium">
{queueItem.singer.name}
</IonChip>
{/* Song Info */}
<IonLabel>
<h3 className="text-sm font-medium text-gray-900 truncate">
{queueItem.song.title}
</h3>
<p className="text-sm text-gray-500 truncate">
{queueItem.song.artist}
</p>
<div className="flex items-center mt-1">
<IonChip color="medium">
{queueItem.singer.name}
</IonChip>
{queueItem.isCurrentUser && (
<IonChip color="primary">
You
</IonChip>
)}
</div>
</IonLabel>
{/* Admin Controls */}
{canReorder && (
<div slot="end" className="flex flex-col gap-1 ml-2">
{queueItem.order > 2 && (
<ActionButton
onClick={() => handleMoveUp(queueItem)}
variant="secondary"
size="sm"
>
<IonIcon icon={arrowUp} />
</ActionButton>
)}
{queueItem.order > 1 && queueItem.order < queueItems.length && (
<ActionButton
onClick={() => handleMoveDown(queueItem)}
variant="secondary"
size="sm"
>
<IonIcon icon={arrowDown} />
</ActionButton>
)}
</div>
)}
</IonItem>
{/* Admin Controls */}
{canReorder && (
<div slot="end" className="flex flex-col gap-1">
{queueItem.order > 2 && (
<ActionButton
onClick={() => handleMoveUp(queueItem)}
variant="secondary"
size="sm"
>
<IonIcon icon={arrowUp} />
</ActionButton>
)}
{queueItem.order > 1 && queueItem.order < queueItems.length && (
<ActionButton
onClick={() => handleMoveDown(queueItem)}
variant="secondary"
size="sm"
>
<IonIcon icon={arrowDown} />
</ActionButton>
)}
</div>
)}
</IonItem>
{/* Swipe Actions */}
{canDelete && (
<IonItemOptions side="end">
<IonItemOption
color="danger"
onClick={() => handleRemoveFromQueue(queueItem)}
>
<IonIcon icon={trash} slot="icon-only" />
</IonItemOption>
</IonItemOptions>
)}
</IonItemSliding>
);
})}
</IonList>
{/* Swipe Actions */}
{canDelete && (
<IonItemOptions side="end">
<IonItemOption
color="danger"
onClick={() => handleRemoveFromQueue(queueItem)}
>
<IonIcon icon={trash} slot="icon-only" />
</IonItemOption>
</IonItemOptions>
)}
</IonItemSliding>
);
};
return (
<>
<PageHeader
title="Queue"
subtitle={`${queueStats.totalSongs} song${queueStats.totalSongs !== 1 ? 's' : ''} in queue`}
/>
<div className="max-w-4xl mx-auto p-6">
{/* Player Controls - Only visible to admin users */}
<div className="mb-6">
<PlayerControls />
</div>
{/* Queue List */}
<InfiniteScrollList<QueueItem>
items={queueItems}
isLoading={queueCount === 0}
hasMore={false}
onLoadMore={() => {}}
renderItem={renderQueueItem}
emptyTitle="Queue is empty"
emptyMessage="Add songs from search, history, or favorites to get started"
loadingTitle="Loading queue..."
loadingMessage="Please wait while queue data is being loaded"
/>
</div>
</div>
</>
);
};

View File

@ -34,12 +34,13 @@ const Search: React.FC = () => {
// Debug logging
console.log('Search component - songs count:', songsCount);
console.log('Search component - search results:', searchResults);
console.log('Search component - search term:', searchTerm);
console.log('Search component - showing:', searchResults.songs.length, 'of', searchResults.count);
return (
<div className="max-w-4xl mx-auto p-6">
<PageHeader
title="Search Songs"
subtitle="Search by title or artist"
/>
<div className="mb-6">
@ -51,11 +52,6 @@ const Search: React.FC = () => {
debounce={300}
showClearButton="focus"
/>
{/* Debug info */}
<div className="mt-2 text-sm text-gray-500">
Total songs loaded: {songsCount} | Showing: {searchResults.songs.length} of {searchResults.count} | Page: {searchResults.currentPage}/{searchResults.totalPages} | Search term: "{searchTerm}"
</div>
</div>
{/* Search Results */}

View File

@ -1,10 +1,11 @@
import React, { useState, useEffect, useRef } from '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 React, { useState } from 'react';
import { IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonChip, IonContent, IonList, IonAccordionGroup, IonAccordion } from '@ionic/react';
import { close, list } from 'ionicons/icons';
import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common';
import { useSongLists } from '../../hooks';
import { useAppSelector } from '../../redux';
import { selectSongList } from '../../redux';
import type { SongListSong, Song } from '../../types';
import type { SongListSong, SongList, Song } from '../../types';
const SongLists: React.FC = () => {
const {
@ -19,237 +20,163 @@ const SongLists: React.FC = () => {
const songListData = useAppSelector(selectSongList);
const songListCount = Object.keys(songListData).length;
const observerRef = useRef<HTMLDivElement>(null);
// Intersection Observer for infinite scrolling
useEffect(() => {
console.log('SongLists - Setting up observer:', { hasMore, songListCount, itemsLength: songLists.length });
const observer = new IntersectionObserver(
(entries) => {
console.log('SongLists - Intersection detected:', {
isIntersecting: entries[0].isIntersecting,
hasMore,
songListCount
});
if (entries[0].isIntersecting && hasMore && songListCount > 0) {
console.log('SongLists - Loading more items');
loadMore();
}
},
{ threshold: 0.1 }
);
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => observer.disconnect();
}, [loadMore, hasMore, songListCount]);
const [selectedSongList, setSelectedSongList] = useState<string | null>(null);
const [expandedSongKey, setExpandedSongKey] = useState<string | null>(null);
// Debug logging - only log when data changes
useEffect(() => {
console.log('SongLists component - songList count:', songListCount);
console.log('SongLists component - songLists:', songLists);
}, [songListCount, songLists.length]);
const handleSongListClick = (songListKey: string) => {
console.log('SongLists - handleSongListClick called with key:', songListKey);
setSelectedSongList(songListKey);
setExpandedSongKey(null); // Reset expansion when opening a new song list
};
const handleCloseSongList = () => {
setSelectedSongList(null);
setExpandedSongKey(null); // Reset expansion when closing
};
const handleSongItemClick = (songKey: string) => {
setExpandedSongKey(expandedSongKey === songKey ? null : songKey);
};
const finalSelectedList = selectedSongList
? allSongLists.find(list => list.key === selectedSongList)
: null;
// Debug logging for modal
useEffect(() => {
console.log('SongLists - Modal state check:', {
selectedSongList,
finalSelectedList: !!finalSelectedList,
songListsLength: songLists.length
});
if (selectedSongList) {
console.log('SongLists - Modal opened for song list:', selectedSongList);
console.log('SongLists - Selected list data:', finalSelectedList);
console.log('SongLists - About to render modal, finalSelectedList:', !!finalSelectedList);
}
}, [selectedSongList, finalSelectedList, songLists.length]);
// Render song list item for InfiniteScrollList
const renderSongListItem = (songList: SongList) => (
<IonItem button onClick={() => handleSongListClick(songList.key!)} detail={false}>
<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>
<IonIcon icon={list} slot="end" color="primary" />
</IonItem>
);
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>
Song Lists
<IonChip color="primary" className="ml-2">
{songLists.length}
</IonChip>
</IonTitle>
</IonToolbar>
</IonHeader>
<PageHeader
title="Song Lists"
subtitle={`${songListCount} items loaded`}
/>
<div className="p-4">
<p className="text-sm text-gray-600 mb-4">
{songLists.length} song list{songLists.length !== 1 ? 's' : ''} available
</p>
{/* Debug info */}
<div className="mb-4 text-sm text-gray-500">
Song lists loaded: {songListCount}
</div>
<div className="max-w-4xl mx-auto p-6">
<InfiniteScrollList<SongList>
items={songLists}
isLoading={songListCount === 0}
hasMore={hasMore}
onLoadMore={loadMore}
renderItem={renderSongListItem}
emptyTitle="No song lists available"
emptyMessage="Song lists will appear here when they're available"
loadingTitle="Loading song lists..."
loadingMessage="Please wait while song lists are being loaded"
/>
{/* 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 */}
<IonModal
isOpen={!!finalSelectedList}
onDidDismiss={handleCloseSongList}
>
<IonHeader>
<IonToolbar>
<IonTitle>{finalSelectedList?.title}</IonTitle>
<IonButton slot="end" fill="clear" onClick={handleCloseSongList}>
<IonIcon icon={close} />
</IonButton>
</IonToolbar>
</IonHeader>
<IonContent>
<IonAccordionGroup value={expandedSongKey}>
{finalSelectedList?.songs.map((songListSong: SongListSong, index) => {
const availableSongs = checkSongAvailability(songListSong);
const isAvailable = availableSongs.length > 0;
const songKey = songListSong.key || `${songListSong.title}-${songListSong.position}-${index}`;
{/* Song List Modal */}
<IonModal isOpen={!!finalSelectedList} onDidDismiss={handleCloseSongList}>
<IonHeader>
<IonToolbar>
<IonTitle>{finalSelectedList?.title}</IonTitle>
<IonButton slot="end" fill="clear" onClick={handleCloseSongList}>
<IonIcon icon={close} />
</IonButton>
</IonToolbar>
</IonHeader>
{/* Remove IonContent, use a div instead */}
<div>
<IonAccordionGroup>
{finalSelectedList?.songs.map((songListSong: SongListSong, idx) => {
const availableSongs = checkSongAvailability(songListSong);
const isAvailable = availableSongs.length > 0;
if (isAvailable) {
// Available songs get an accordion that expands
return (
<IonAccordion key={songKey} value={songKey} style={{ '--border-style': 'none' } as React.CSSProperties}>
<IonItem
slot="header"
detail={false}
button
onClick={() => handleSongItemClick(songKey)}
style={{
'--border-style': 'solid',
'--border-width': '0 0 1px 0',
'--border-color': 'rgba(0, 0, 0, 0.13)'
} as React.CSSProperties}
>
{/* Number */}
<div slot="start" className="flex-shrink-0 w-12 h-12 flex items-center justify-center text-gray-600 font-medium">
{index + 1})
</div>
return (
<IonAccordion key={songListSong.key || `${songListSong.title}-${songListSong.position}-${idx}`} value={songListSong.key}>
<IonItem slot="header" className={!isAvailable ? 'opacity-50' : ''}>
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
{songListSong.title}
</h3>
<p className="text-sm text-gray-500">
{songListSong.artist} Position {songListSong.position}
</p>
{!isAvailable && (
<p className="text-xs text-red-500 mt-1">
Not available in catalog
</p>
)}
</IonLabel>
{isAvailable && (
<IonChip slot="end" color="success">
{availableSongs.length} version{availableSongs.length !== 1 ? 's' : ''}
</IonChip>
)}
</IonItem>
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
{songListSong.artist}
</h3>
<p className="text-sm text-gray-500">
{songListSong.title}
</p>
</IonLabel>
<div slot="content">
{isAvailable ? (
<IonList>
{availableSongs.map((song: Song, sidx) => (
<IonItem key={song.key || `${song.title}-${song.artist}-${sidx}`}>
<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 className="p-4 text-center text-gray-500">
No matching songs found in catalog
<IonChip slot="end" color="success">
{availableSongs.length} version{availableSongs.length !== 1 ? 's' : ''}
</IonChip>
</IonItem>
<div slot="content" className="bg-gray-50 border-l-4 border-primary">
<IonList>
{availableSongs.map((song: Song, sidx) => (
<SongItem
key={song.key || `${song.title}-${song.artist}-${sidx}`}
song={song}
context="search"
onAddToQueue={() => handleAddToQueue(song)}
onToggleFavorite={() => handleToggleFavorite(song)}
/>
))}
</IonList>
</div>
)}
</div>
</IonAccordion>
);
})}
</IonAccordionGroup>
</div>
</IonModal>
</IonAccordion>
);
} else {
// Unavailable songs get a simple item
return (
<IonItem
key={songKey}
detail={false}
className="opacity-50"
>
{/* Number */}
<div slot="start" className="flex-shrink-0 w-12 h-12 flex items-center justify-center text-gray-600 font-medium">
{index + 1})
</div>
<IonLabel>
<h3 className="text-sm font-medium text-gray-400">
{songListSong.artist}
</h3>
<p className="text-sm text-gray-300">
{songListSong.title}
</p>
</IonLabel>
</IonItem>
);
}
})}
</IonAccordionGroup>
</IonContent>
</IonModal>
</div>
</>
);
};

View File

@ -1,5 +1,4 @@
import React from 'react';
import { IonHeader, IonToolbar, IonTitle, IonChip } from '@ionic/react';
import { useTopPlayed } from '../../hooks';
import { useAppSelector } from '../../redux';
import { selectTopPlayed } from '../../redux';
@ -99,17 +98,6 @@ const Top100: React.FC = () => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>
Top 100 Played
<IonChip color="primary" className="ml-2">
{displayItems.length}
</IonChip>
</IonTitle>
</IonToolbar>
</IonHeader>
<PageHeader
title="Top 100 Played"
subtitle={`${displayCount} items loaded (Mock Data)`}