diff --git a/src/features/SongLists/SongLists.tsx b/src/features/SongLists/SongLists.tsx index 2df501a..448fb7b 100644 --- a/src/features/SongLists/SongLists.tsx +++ b/src/features/SongLists/SongLists.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo, useCallback } 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'; @@ -35,14 +35,27 @@ const SongLists: React.FC = () => { setExpandedSongKey(null); // Reset expansion when closing }; - const handleSongItemClick = (songKey: string) => { + const handleSongItemClick = useCallback((songKey: string) => { setExpandedSongKey(expandedSongKey === songKey ? null : songKey); - }; + }, [expandedSongKey]); const finalSelectedList = selectedSongList ? allSongLists.find(list => list.key === selectedSongList) : null; + // Pre-calculate available songs for the selected list to avoid repeated calculations + const selectedListWithAvailability = useMemo(() => { + if (!finalSelectedList) return null; + + return { + ...finalSelectedList, + songs: finalSelectedList.songs.map(songListSong => ({ + ...songListSong, + availableSongs: checkSongAvailability(songListSong) + })) + }; + }, [finalSelectedList, checkSongAvailability]); + // Render song list item for InfiniteScrollList const renderSongListItem = (songList: SongList) => ( handleSongListClick(songList.key!)} detail={false}> @@ -94,26 +107,16 @@ const SongLists: React.FC = () => { - {finalSelectedList?.songs.map((songListSong: SongListSong, index) => { - const availableSongs = checkSongAvailability(songListSong); + {selectedListWithAvailability?.songs.map((songListSong: SongListSong & { availableSongs: Song[] }, index) => { + const availableSongs = songListSong.availableSongs; const isAvailable = availableSongs.length > 0; const songKey = songListSong.key || `${songListSong.title}-${songListSong.position}-${index}`; if (isAvailable) { // Available songs get an accordion that expands return ( - - handleSongItemClick(songKey)} - style={{ - '--border-style': 'solid', - '--border-width': '0 0 1px 0', - '--border-color': 'rgba(0, 0, 0, 0.13)' - } as React.CSSProperties} - > + + handleSongItemClick(songKey)}> {/* Number */}
{index + 1}) diff --git a/src/features/TopPlayed/Top100.tsx b/src/features/TopPlayed/Top100.tsx index 8a552b6..dd684d9 100644 --- a/src/features/TopPlayed/Top100.tsx +++ b/src/features/TopPlayed/Top100.tsx @@ -1,9 +1,14 @@ -import React from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; +import { IonItem, IonLabel, IonChip, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent, IonList } from '@ionic/react'; +import { close, list } from 'ionicons/icons'; import { useTopPlayed } from '../../hooks'; import { useAppSelector } from '../../redux'; -import { selectTopPlayed } from '../../redux'; -import { InfiniteScrollList, PageHeader } from '../../components/common'; -import type { TopPlayed } from '../../types'; +import { selectTopPlayed, selectSongsArray } from '../../redux'; +import { InfiniteScrollList, PageHeader, SongItem } from '../../components/common'; +import { filterSongs } from '../../utils/dataProcessing'; +import { useSongOperations } from '../../hooks'; +import { useToast } from '../../hooks'; +import type { TopPlayed, Song } from '../../types'; const Top100: React.FC = () => { console.log('Top100 component - RENDERING START'); @@ -15,9 +20,62 @@ const Top100: React.FC = () => { const topPlayed = useAppSelector(selectTopPlayed); const topPlayedCount = Object.keys(topPlayed).length; + const allSongs = useAppSelector(selectSongsArray); + const { addToQueue, toggleFavorite } = useSongOperations(); + const { showSuccess, showError } = useToast(); + const [selectedTopPlayed, setSelectedTopPlayed] = useState(null); console.log('Top100 component - Redux data:', { topPlayedCount, topPlayedItems: topPlayedItems.length }); + const handleTopPlayedClick = useCallback((item: TopPlayed) => { + setSelectedTopPlayed(item); + }, []); + + const handleCloseModal = useCallback(() => { + setSelectedTopPlayed(null); + }, []); + + // Find songs that match the selected top played item + const selectedSongs = useMemo(() => { + if (!selectedTopPlayed) return []; + + // Use the shared search function with title and artist + const searchTerm = `${selectedTopPlayed.title} ${selectedTopPlayed.artist}`; + + console.log('Top100 - Search details:', { + selectedTopPlayed, + searchTerm, + allSongsCount: allSongs.length + }); + + const filteredSongs = filterSongs(allSongs, searchTerm); + + console.log('Top100 - Search results:', { + filteredSongsCount: filteredSongs.length, + firstFewResults: filteredSongs.slice(0, 3).map(s => `${s.artist} - ${s.title}`) + }); + + return filteredSongs; + }, [selectedTopPlayed, allSongs]); + + const handleAddToQueue = useCallback(async (song: Song) => { + try { + await addToQueue(song); + showSuccess('Song added to queue'); + } catch { + showError('Failed to add song to queue'); + } + }, [addToQueue, showSuccess, showError]); + + const handleToggleFavorite = useCallback(async (song: Song) => { + try { + await toggleFavorite(song); + showSuccess(song.favorite ? 'Removed from favorites' : 'Added to favorites'); + } catch { + showError('Failed to update favorites'); + } + }, [toggleFavorite, showSuccess, showError]); + // Mock data for testing - these are artist/title combinations, not individual songs const mockTopPlayedItems: TopPlayed[] = [ { @@ -109,26 +167,64 @@ const Top100: React.FC = () => { hasMore={displayHasMore} onLoadMore={loadMore} renderItem={(item, index) => ( -
-
- - {index + 1}) - + handleTopPlayedClick(item)} + detail={false} + > + {/* Number */} +
+ {index + 1})
-
-
-
- {item.title} ({item.count}) -
-
+ + +

+ {item.title} +

+

{item.artist} -

-
-
+

+ + + + {item.count} plays + + + + )} emptyTitle="No top played songs" emptyMessage="Play some songs to see the top played list" /> + + {/* Top Played Songs Modal */} + + + + {selectedTopPlayed?.artist} + + + + + + + + + {selectedSongs.map((song) => ( + handleAddToQueue(song)} + onToggleFavorite={() => handleToggleFavorite(song)} + /> + ))} + + + ); }; diff --git a/src/index.css b/src/index.css index aea3914..f6c5370 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,4 @@ -/* Ionic CSS imports */ +/* Ionic CSS imports - must come first to establish base styles */ @import '@ionic/react/css/core.css'; @import '@ionic/react/css/normalize.css'; @import '@ionic/react/css/structure.css'; @@ -11,7 +11,7 @@ @import '@ionic/react/css/flex-utils.css'; @import '@ionic/react/css/display.css'; -/* Tailwind CSS */ +/* Tailwind CSS - comes after Ionic to avoid overriding essential Ionic styles */ @tailwind base; @tailwind components; @tailwind utilities; @@ -33,3 +33,36 @@ ion-item.ion-activated { ion-menu { --z-index: 1000; } + +/* Ensure modal content is visible */ +ion-modal ion-content { + --background: var(--ion-background-color, #ffffff); +} + +ion-modal ion-list { + background: transparent; +} + +/* Ensure content is visible and properly styled */ +ion-content { + --background: var(--ion-background-color, #ffffff); +} + +ion-list { + background: transparent; +} + +/* Ensure items are properly displayed */ +ion-item { + --background: transparent; + --color: var(--ion-text-color, #000000); +} + +/* Ensure accordion content is visible */ +ion-accordion { + --background: transparent; +} + +ion-accordion ion-item { + --background: transparent; +} diff --git a/src/utils/dataProcessing.ts b/src/utils/dataProcessing.ts index 008eb9c..dec1204 100644 --- a/src/utils/dataProcessing.ts +++ b/src/utils/dataProcessing.ts @@ -10,15 +10,28 @@ export const objectToArray = ( })); }; -// Filter songs by search term +// Filter songs by search term with intelligent multi-word handling export const filterSongs = (songs: Song[], searchTerm: string): Song[] => { if (!searchTerm.trim()) return songs; - const term = searchTerm.toLowerCase(); - return songs.filter(song => - song.title.toLowerCase().includes(term) || - song.artist.toLowerCase().includes(term) - ); + const terms = searchTerm.toLowerCase().split(/\s+/).filter(term => term.length > 0); + + if (terms.length === 0) return songs; + + return songs.filter(song => { + const songTitle = song.title.toLowerCase(); + const songArtist = song.artist.toLowerCase(); + + // If only one term, use OR logic (title OR artist) + if (terms.length === 1) { + return songTitle.includes(terms[0]) || songArtist.includes(terms[0]); + } + + // If multiple terms, use AND logic (all terms must match somewhere) + return terms.every(term => + songTitle.includes(term) || songArtist.includes(term) + ); + }); }; // Sort queue items by order