Skip to content

Commit 3c195ae

Browse files
committed
feat: Enhance useTabService composable with artist and song processing improvements
- Introduced a new helper function `_processArtists` to streamline artist data processing and ensure clean serialization. - Updated `getImageUrl` to utilize a pre-initialized function for better SSR context handling. - Enhanced song processing to ensure plain objects are returned for serialization, including handling rhythm and artist data. - Added explicit field projection in data fetching to ensure necessary fields like images are returned. - Improved search functionality to include artist name queries in addition to song titles.
1 parent 5844df5 commit 3c195ae

5 files changed

Lines changed: 119 additions & 83 deletions

File tree

end_user/composables/useTabService.ts

Lines changed: 108 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const getRandomElement = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.
1313
* Composable for tab/song related services
1414
*/
1515
export 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(', ')

end_user/pages/artist/index.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const { data: artistsData, pending: isLoading, refresh: refreshArtists } = await
5252
getArtistsKey,
5353
async () => {
5454
// Get pagination controller
55+
let initialDocs: Artist[] = []
5556
const controller = fetchAllArtists(
5657
{
5758
limit: itemsPerPage,
@@ -60,6 +61,7 @@ const { data: artistsData, pending: isLoading, refresh: refreshArtists } = await
6061
search: searchQuery.value.trim() || undefined,
6162
},
6263
(docs) => {
64+
initialDocs = docs
6365
artists.value = docs
6466
}
6567
)
@@ -78,7 +80,7 @@ const { data: artistsData, pending: isLoading, refresh: refreshArtists } = await
7880
paginationController.value = controller
7981
8082
return {
81-
artists: artists.value,
83+
artists: initialDocs.length > 0 ? initialDocs : artists.value,
8284
totalResults: total,
8385
totalPages: pages,
8486
}
@@ -185,6 +187,7 @@ const getArtistName = (artist: Artist) => {
185187
<div v-else-if="artists.length > 0" class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 3xl:grid-cols-6 gap-6">
186188
<ArtistCard v-for="artist in artists" :key="artist._id" :name="getArtistName(artist)"
187189
:song-count="artist.chords || 0" :songs-label="t('common.songs')"
190+
:avatar-url="useTabService().getImageUrl(artist.image)" :gradient-border="(artist as any)._mockColor"
188191
@click="navigateToArtist(artist._id || '')" />
189192
</div>
190193

0 commit comments

Comments
 (0)