11import React , { memo , useMemo , useCallback } from "react" ;
22import { ApiHelper , Loading , Locale , PageHeader , UserHelper , Permissions } from "@churchapps/apphelper" ;
33import { Link , Navigate } from "react-router-dom" ;
4- import { Button , Box , Card , CardContent , Typography , Stack , Avatar , Chip , IconButton , TextField , InputAdornment , Tooltip , Checkbox } from "@mui/material" ;
4+ import { Button , Box , Card , CardContent , Typography , Stack , Avatar , Chip , IconButton , TextField , InputAdornment , Tooltip , Checkbox , TablePagination } from "@mui/material" ;
55import { MusicNote as MusicIcon , LibraryMusic as LibraryIcon , Add as AddIcon , Search as SearchIcon , PlayCircle as PlayIcon , Timer as TimerIcon , Person as ArtistIcon , Delete as DeleteIcon } from "@mui/icons-material" ;
66import { SongSearchDialog } from "./SongSearchDialog" ;
77import { EmptyState } from "../../components/ui/EmptyState" ;
@@ -20,11 +20,24 @@ export const SongsPage = memo(() => {
2020 const [ selected , setSelected ] = React . useState < Set < string > > ( new Set ( ) ) ;
2121 const { confirm, ConfirmDialogElement } = useConfirmDelete ( ) ;
2222
23- const songs = useQuery < SongDetailInterface [ ] > ( {
24- queryKey : [ "/songDetails" , "ContentApi" ] ,
25- placeholderData : [ ]
23+ const [ page , setPage ] = React . useState ( 0 ) ;
24+ const [ rowsPerPage , setRowsPerPage ] = React . useState ( 10 ) ;
25+
26+ const songs = useQuery < { songDetails : SongDetailInterface [ ] , count : number } > ( {
27+ queryKey : [ `/songDetails?limit=${ rowsPerPage } &offset=${ page * rowsPerPage } &search=${ searchFilter } ` , "ContentApi" ] ,
28+ placeholderData : { songDetails : [ ] , count : 0 }
2629 } ) ;
2730
31+ const handlePageChange = useCallback ( ( _ : unknown , newPage : number ) => {
32+ setPage ( newPage ) ;
33+ } , [ ] ) ;
34+
35+ const handleRowsPerPageChange = useCallback ( ( e : React . ChangeEvent < HTMLInputElement > ) => {
36+ const newLimit = parseInt ( e . target . value , 10 ) ;
37+ setRowsPerPage ( newLimit ) ;
38+ setPage ( 0 ) ;
39+ } , [ ] ) ;
40+
2841 const handleAdd = useCallback (
2942 async ( songDetail : SongDetailInterface ) => {
3043 let selectedSong ;
@@ -91,25 +104,26 @@ export const SongsPage = memo(() => {
91104 } , [ ] ) ;
92105
93106 const filteredSongs = useMemo ( ( ) => {
94- if ( ! songs . data ) return null ;
107+ const songList = songs . data ?. songDetails ;
108+ if ( ! songList ) return null ;
95109 // Dedupe by songId: /songDetails join produces one row per arrangement.
96110 const seen = new Set < string > ( ) ;
97- const unique = songs . data . filter ( ( song ) => {
111+ const unique = songList . filter ( ( song ) => {
98112 const id = ( song as any ) . songId || song . id ;
99113 if ( seen . has ( id ) ) return false ;
100114 seen . add ( id ) ;
101115 return true ;
102116 } ) ;
103- if ( ! searchFilter . trim ( ) ) return unique ;
104-
105- const filter = searchFilter . toLowerCase ( ) ;
106- return unique . filter ( ( song ) => song . title ?. toLowerCase ( ) . includes ( filter ) || song . artist ?. toLowerCase ( ) . includes ( filter ) ) ;
107- } , [ songs . data , searchFilter ] ) ;
117+ return unique ;
118+ } , [ songs . data ?. songDetails ] ) ;
108119
109120 const songsContent = useMemo ( ( ) => {
110121 if ( songs . isLoading ) return < Loading size = "sm" /> ;
111122
112- if ( ( songs . data ?. length ?? 0 ) === 0 ) {
123+ if ( ( songs . data ?. songDetails ?. length ?? 0 ) === 0 ) {
124+ if ( searchFilter . trim ( ) ) {
125+ return < EmptyState icon = { < SearchIcon /> } title = { Locale . label ( "songs.library.noResults" ) || "No songs match your search criteria." } /> ;
126+ }
113127 return (
114128 < EmptyState
115129 icon = { < LibraryIcon /> }
@@ -189,10 +203,21 @@ export const SongsPage = memo(() => {
189203 </ Card >
190204 ) ) }
191205 </ Stack >
206+ < TablePagination
207+ component = "div"
208+ count = { songs . data ?. count ?? 0 }
209+ page = { page }
210+ onPageChange = { handlePageChange }
211+ rowsPerPage = { rowsPerPage }
212+ onRowsPerPageChange = { handleRowsPerPageChange }
213+ rowsPerPageOptions = { [ 10 , 25 , 50 ] }
214+ sx = { { mt : 2 } }
215+ />
192216 </ Box >
193217 ) ;
194218 } , [
195- songs . isLoading , songs . data , filteredSongs , formatSeconds , handleImageError , failedImages , canEdit , selected , toggleSelected
219+ songs . isLoading , songs . data , filteredSongs , formatSeconds , handleImageError , failedImages , canEdit , selected , toggleSelected ,
220+ page , rowsPerPage , handlePageChange , handleRowsPerPageChange
196221 ] ) ;
197222
198223 if ( redirect ) return < Navigate to = { redirect } /> ;
@@ -221,7 +246,7 @@ export const SongsPage = memo(() => {
221246 </ PageHeader >
222247
223248 < Box sx = { { p : 3 } } >
224- { ( showSearchField || searchFilter ) && songs . data && songs . data . length > 0 && (
249+ { ( showSearchField || searchFilter ) && songs . data && ( ( songs . data . songDetails ?. length ?? 0 ) > 0 || searchFilter ) && (
225250 < Card sx = { { mb : 3 , borderRadius : 2 , border : "1px solid" , borderColor : "divider" } } >
226251 < CardContent sx = { { pb : 2 , "&:last-child" : { pb : 2 } } } >
227252 < Stack direction = "row" alignItems = "center" spacing = { 1 } sx = { { mb : 2 } } >
@@ -235,7 +260,10 @@ export const SongsPage = memo(() => {
235260 variant = "outlined"
236261 placeholder = { Locale . label ( "songs.search.placeholder" ) || "Search songs by title or artist..." }
237262 value = { searchFilter }
238- onChange = { ( e ) => setSearchFilter ( e . target . value ) }
263+ onChange = { ( e ) => {
264+ setSearchFilter ( e . target . value ) ;
265+ setPage ( 0 ) ;
266+ } }
239267 InputProps = { {
240268 startAdornment : (
241269 < InputAdornment position = "start" >
0 commit comments