@@ -13,6 +13,8 @@ const getRandomElement = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.
1313 * Composable for tab/song related services
1414 */
1515export const useTabService = ( ) => {
16+ const { getImageUrl : _getImageUrl } = useImageUrl ( )
17+
1618 /**
1719 * Fetch songs with limited fields optimized for SongCard
1820 */
@@ -98,26 +100,18 @@ export const useTabService = () => {
98100 query : { } ,
99101 options : {
100102 limit : 10 ,
103+ // Explicitly project fields to ensure image is returned
104+ projection : {
105+ content : 1 ,
106+ chords : 1 ,
107+ image : 1 ,
108+ createdAt : 1 ,
109+ updatedAt : 1
110+ }
101111 } ,
102112 } )
103113
104- if ( ! artists || artists . length === 0 ) {
105- return [ ]
106- }
107-
108- return artists . map ( ( artist ) => {
109- // Mock color for gradient border
110- // eslint-disable-next-line @typescript-eslint/no-explicit-any
111- ; ( artist as any ) . _mockColor = getRandomElement ( [
112- 'from-pink-500 to-rose-500' ,
113- 'from-purple-500 to-indigo-500' ,
114- 'from-blue-500 to-cyan-500' ,
115- 'from-orange-500 to-red-500' ,
116- 'from-emerald-500 to-teal-500' ,
117- ] )
118-
119- return artist
120- } )
114+ return _processArtists ( artists || [ ] )
121115 } catch ( error ) {
122116 console . error ( 'Failed to fetch featured artists:' , error )
123117 return [ ]
@@ -144,9 +138,33 @@ export const useTabService = () => {
144138
145139 // Note: getImageUrl has been moved to useImageUrl composable
146140 // This is kept for backward compatibility but delegates to useImageUrl
141+ // Updated to use the pre-initialized _getImageUrl to preserve Nuxt context during SSR
147142 const getImageUrl = ( file : any ) => {
148- const { getImageUrl : getImageUrlFromComposable } = useImageUrl ( )
149- return getImageUrlFromComposable ( file )
143+ return _getImageUrl ( file ) ;
144+ }
145+
146+ // Helper to process artists with mock data and ensure clean objects for serialization
147+ const _processArtists = ( artists : Artist [ ] ) : Artist [ ] => {
148+ if ( ! artists || artists . length === 0 ) {
149+ return [ ]
150+ }
151+
152+ return artists . map ( ( artist ) => {
153+ // Create a plain object to ensure clean serialization through useAsyncData
154+ const plainArtist = { ...artist }
155+
156+ // Mock color for gradient border
157+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
158+ ; ( plainArtist as any ) . _mockColor = getRandomElement ( [
159+ 'from-pink-500 to-rose-500' ,
160+ 'from-purple-500 to-indigo-500' ,
161+ 'from-blue-500 to-cyan-500' ,
162+ 'from-orange-500 to-red-500' ,
163+ 'from-emerald-500 to-teal-500' ,
164+ ] )
165+
166+ return plainArtist
167+ } )
150168 }
151169
152170 // Helper to process songs with mock data and extract default language content
@@ -156,21 +174,29 @@ export const useTabService = () => {
156174 }
157175
158176 return songs . map ( ( song ) => {
159- // If it's already a SongWithPopulatedRefs (old structure), return as is
160- if ( 'title' in song && typeof song . title === 'string' ) {
161- const songWithRefs = song as unknown as SongWithPopulatedRefs
162-
163- // Handle rhythm - extract titles from Rhythm[] array
164- if ( songWithRefs . rhythm && Array . isArray ( songWithRefs . rhythm ) ) {
165- const rhythmStr = songWithRefs . rhythm
166- . map ( r => r ?. title || '' )
167- . filter ( Boolean )
168- . join ( ', ' )
169- songWithRefs . rhythm = rhythmStr || undefined
170- } else {
171- songWithRefs . rhythm = undefined
177+ // Ensure we're working with a plain object for serialization
178+ const songDoc = { ...song }
179+
180+ // Also process populated artists if they exist
181+ if ( songDoc . artists && Array . isArray ( songDoc . artists ) && songDoc . artists . length > 0 && typeof songDoc . artists [ 0 ] === 'object' ) {
182+ songDoc . artists = _processArtists ( songDoc . artists as Artist [ ] )
172183 }
173184
185+ // If it's already a SongWithPopulatedRefs (old structure), return as is
186+ if ( 'title' in songDoc && typeof songDoc . title === 'string' ) {
187+ const songWithRefs = songDoc as unknown as SongWithPopulatedRefs
188+
189+ // Handle rhythm - extract titles from Rhythm[] array
190+ if ( songWithRefs . rhythm && Array . isArray ( songWithRefs . rhythm ) ) {
191+ const rhythmStr = ( songWithRefs . rhythm as any [ ] )
192+ . map ( r => r ?. title || '' )
193+ . filter ( Boolean )
194+ . join ( ', ' )
195+ songWithRefs . rhythm = rhythmStr || undefined
196+ } else {
197+ songWithRefs . rhythm = undefined
198+ }
199+
174200 // Ensure chords object exists
175201 if ( ! songWithRefs . chords ) {
176202 songWithRefs . chords = { list : [ ] }
@@ -184,13 +210,13 @@ export const useTabService = () => {
184210 }
185211
186212 // New structure: extract default language content
187- const songWithContent = song as Song
213+ const songWithContent = songDoc as Song
188214 const langContent = songWithContent . content ?. [ 'ckb-IR' ]
189215
190216 if ( ! langContent ) {
191217 // Fallback: try to find any available language
192218 const availableLang = Object . keys ( songWithContent . content || { } ) . find (
193- lang => songWithContent . content ?. [ lang as LanguageCode ] ?. title
219+ lang => ( songWithContent . content as any ) ?. [ lang ] ?. title
194220 ) as LanguageCode | undefined
195221
196222 if ( ! availableLang ) {
@@ -206,7 +232,7 @@ export const useTabService = () => {
206232 } as SongWithPopulatedRefs
207233 }
208234
209- const fallbackContent = songWithContent . content [ availableLang ]
235+ const fallbackContent = ( songWithContent . content as any ) [ availableLang ]
210236 return {
211237 _id : songWithContent . _id ,
212238 title : fallbackContent ?. title || '' ,
@@ -215,15 +241,15 @@ export const useTabService = () => {
215241 if ( ! songWithContent . rhythm || ! Array . isArray ( songWithContent . rhythm ) ) {
216242 return undefined
217243 }
218- const rhythmStr = songWithContent . rhythm
244+ const rhythmStr = ( songWithContent . rhythm as any [ ] )
219245 . map ( r => r ?. title || '' )
220246 . filter ( Boolean )
221247 . join ( ', ' )
222248 return rhythmStr || undefined
223249 } ) ( ) ,
224250 sections : fallbackContent ?. sections ,
225- artists : songWithContent . artists ,
226- genres : songWithContent . genres ,
251+ artists : songWithContent . artists as any ,
252+ genres : songWithContent . genres as any ,
227253 chords : songWithContent . chords || { list : [ ] } ,
228254 image : songWithContent . image ,
229255 melodies : songWithContent . melodies ,
@@ -239,15 +265,15 @@ export const useTabService = () => {
239265 if ( ! songWithContent . rhythm || ! Array . isArray ( songWithContent . rhythm ) ) {
240266 return undefined
241267 }
242- const rhythmStr = songWithContent . rhythm
268+ const rhythmStr = ( songWithContent . rhythm as any [ ] )
243269 . map ( r => r ?. title || '' )
244270 . filter ( Boolean )
245271 . join ( ', ' )
246272 return rhythmStr || undefined
247273 } ) ( ) ,
248274 sections : langContent . sections ,
249- artists : songWithContent . artists ,
250- genres : songWithContent . genres ,
275+ artists : songWithContent . artists as any ,
276+ genres : songWithContent . genres as any ,
251277 chords : songWithContent . chords || { list : [ ] } ,
252278 image : songWithContent . image ,
253279 melodies : songWithContent . melodies ,
@@ -277,7 +303,6 @@ export const useTabService = () => {
277303 const songs = await dataProvider . find < Song > ( {
278304 database : DATABASE_NAME ,
279305 collection : COLLECTION_NAME . SONG ,
280- populates : [ 'artists' , 'rhythm' ] ,
281306 query : {
282307 $or : [
283308 { 'content.ckb-IR.title' : { $regex : query , $options : 'i' } } ,
@@ -287,7 +312,9 @@ export const useTabService = () => {
287312 } ,
288313 options : {
289314 limit,
290- select : {
315+ // @ts -expect-error: populate is not in the strict type definition but supported by backend
316+ populate : [ 'artists' , 'rhythm' ] ,
317+ projection : {
291318 'content.ckb-IR.title' : 1 ,
292319 'content.ckb-Latn.title' : 1 ,
293320 'content.kmr.title' : 1 ,
@@ -333,12 +360,15 @@ export const useTabService = () => {
333360 const queryObj : any = { }
334361 const orConditions : any [ ] = [ ]
335362
336- // Text search - search in all language content titles
363+ // Text search - search in all language content titles and artist names
337364 if ( query && query . trim ( ) . length > 0 ) {
338365 orConditions . push (
339366 { 'content.ckb-IR.title' : { $regex : query , $options : 'i' } } ,
340367 { 'content.ckb-Latn.title' : { $regex : query , $options : 'i' } } ,
341- { 'content.kmr.title' : { $regex : query , $options : 'i' } }
368+ { 'content.kmr.title' : { $regex : query , $options : 'i' } } ,
369+ { 'artists.content.ckb-IR.name' : { $regex : query , $options : 'i' } } ,
370+ { 'artists.content.ckb-Latn.name' : { $regex : query , $options : 'i' } } ,
371+ { 'artists.content.kmr.name' : { $regex : query , $options : 'i' } }
342372 )
343373 }
344374
@@ -396,9 +426,10 @@ export const useTabService = () => {
396426 database : DATABASE_NAME ,
397427 collection : COLLECTION_NAME . SONG ,
398428 query : queryObj ,
399- populates : [ 'artists' , 'rhythm' ] ,
400429 options : {
401- select : {
430+ // @ts -expect-error: populate is not in the strict type definition but supported by backend
431+ populate : [ 'artists' , 'rhythm' ] ,
432+ projection : {
402433 'content.ckb-IR.title' : 1 ,
403434 'content.ckb-Latn.title' : 1 ,
404435 'content.kmr.title' : 1 ,
@@ -479,25 +510,21 @@ export const useTabService = () => {
479510 query : queryObj ,
480511 options : {
481512 sort : sortObj ,
513+ // Explicitly project fields to ensure image is returned
514+ projection : {
515+ content : 1 ,
516+ chords : 1 ,
517+ image : 1 ,
518+ createdAt : 1 ,
519+ updatedAt : 1
520+ }
482521 } ,
483522 } ,
484523 {
485524 limit,
486525 page,
487526 onFetched : ( docs ) => {
488- const processedArtists = docs . map ( ( artist ) => {
489- // Mock color for gradient border
490- // eslint-disable-next-line @typescript-eslint/no-explicit-any
491- ; ( artist as any ) . _mockColor = getRandomElement ( [
492- 'from-pink-500 to-rose-500' ,
493- 'from-purple-500 to-indigo-500' ,
494- 'from-blue-500 to-cyan-500' ,
495- 'from-orange-500 to-red-500' ,
496- 'from-emerald-500 to-teal-500' ,
497- ] )
498-
499- return artist
500- } )
527+ const processedArtists = _processArtists ( docs )
501528 onFetched ( processedArtists )
502529 } ,
503530 }
@@ -510,23 +537,24 @@ export const useTabService = () => {
510537 database : DATABASE_NAME ,
511538 collection : COLLECTION_NAME . ARTIST ,
512539 query : { _id : id } ,
513- options : { limit : 1 } ,
540+ options : {
541+ limit : 1 ,
542+ // Explicitly project fields to ensure image is returned
543+ projection : {
544+ content : 1 ,
545+ chords : 1 ,
546+ image : 1 ,
547+ createdAt : 1 ,
548+ updatedAt : 1
549+ }
550+ } ,
514551 } )
515552
516553 const artist = artists && artists . length > 0 ? artists [ 0 ] : null
517554
518555 if ( artist ) {
519- // Mock color for gradient border
520- // eslint-disable-next-line @typescript-eslint/no-explicit-any
521- ; ( artist as any ) . _mockColor = getRandomElement ( [
522- 'from-pink-500 to-rose-500' ,
523- 'from-purple-500 to-indigo-500' ,
524- 'from-blue-500 to-cyan-500' ,
525- 'from-orange-500 to-red-500' ,
526- 'from-emerald-500 to-teal-500' ,
527- ] )
528-
529- return artist as Artist
556+ const processed = _processArtists ( [ artist ] )
557+ return processed [ 0 ] as Artist
530558 }
531559
532560 return null
@@ -541,12 +569,13 @@ export const useTabService = () => {
541569 const songs = await dataProvider . find < SongWithPopulatedRefs > ( {
542570 database : DATABASE_NAME ,
543571 collection : COLLECTION_NAME . SONG ,
544- populates : [ 'artists' , 'rhythm' ] ,
545572 query : {
546573 artists : artistId ,
547574 } ,
548575 options : {
549- select : {
576+ // @ts -expect-error: populate is not in the strict type definition but supported by backend
577+ populate : [ 'artists' , 'rhythm' ] ,
578+ projection : {
550579 'content.ckb-IR.title' : 1 ,
551580 'content.ckb-Latn.title' : 1 ,
552581 'content.kmr.title' : 1 ,
@@ -570,11 +599,11 @@ export const useTabService = () => {
570599 const song = await dataProvider . findOne < Song > ( {
571600 database : DATABASE_NAME ,
572601 collection : COLLECTION_NAME . SONG ,
573- // @ts -expect-error: populate is not in the strict type definition but supported by backend
574- populates : [ 'artists' , 'genres' , 'rhythm' ] ,
575602 query : { _id : id } ,
576603 options : {
577604 limit : 1 ,
605+ // @ts -expect-error: populate is not in the strict type definition but supported by backend
606+ populate : [ 'artists' , 'genres' , 'rhythm' ] ,
578607 } ,
579608 } )
580609
@@ -584,7 +613,7 @@ export const useTabService = () => {
584613
585614 // Extract language-specific content (direct access - O(1))
586615 const targetLang = lang || 'ckb-IR'
587- const langContent = song . content ?. [ targetLang ]
616+ const langContent = ( song . content as any ) ?. [ targetLang ]
588617
589618 if ( ! langContent ) {
590619 // Fallback to default language
@@ -600,7 +629,7 @@ export const useTabService = () => {
600629 title_seo : defaultContent . title_seo ,
601630 rhythm : ( ( ) => {
602631 if ( ! song . rhythm || ! Array . isArray ( song . rhythm ) ) return ''
603- return song . rhythm
632+ return ( song . rhythm as any [ ] )
604633 . map ( r => r ?. title || '' )
605634 . filter ( Boolean )
606635 . join ( ', ' )
@@ -617,7 +646,7 @@ export const useTabService = () => {
617646 title_seo : langContent . title_seo ,
618647 rhythm : ( ( ) => {
619648 if ( ! song . rhythm || ! Array . isArray ( song . rhythm ) ) return ''
620- return song . rhythm
649+ return ( song . rhythm as any [ ] )
621650 . map ( r => r ?. title || '' )
622651 . filter ( Boolean )
623652 . join ( ', ' )
0 commit comments