From fb4fbcbe42c5cbf1dbb7294fe3f9dc0fce26b7c5 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Thu, 18 Sep 2025 21:28:37 -0300 Subject: [PATCH 01/32] feat: add song search functionality with pagination and sorting, update API routes for Swagger documentation --- apps/backend/src/main.ts | 4 +- apps/backend/src/song/song.controller.ts | 11 +++++ apps/backend/src/song/song.service.ts | 42 +++++++++++++++++++ .../database/src/song/dto/SongView.dto.ts | 1 + 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 6a3950c1..389ee3c0 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -10,7 +10,7 @@ const logger: Logger = new Logger('main.ts'); async function bootstrap() { const app = await NestFactory.create(AppModule); - app.setGlobalPrefix('api/v1'); + app.setGlobalPrefix('v1'); const parseTokenPipe = app.get(ParseTokenPipe); @@ -56,7 +56,7 @@ bootstrap() logger.warn(`Application is running on: http://localhost:${port}`); if (process.env.NODE_ENV === 'development') { - logger.warn(`Swagger is running on: http://localhost:${port}/api/doc`); + logger.warn(`Swagger is running on: http://localhost:${port}/docs`); } }) .catch((error) => { diff --git a/apps/backend/src/song/song.controller.ts b/apps/backend/src/song/song.controller.ts index d6711cee..0c4f3bb1 100644 --- a/apps/backend/src/song/song.controller.ts +++ b/apps/backend/src/song/song.controller.ts @@ -76,6 +76,17 @@ export class SongController { return await this.songService.getSongByPage(query); } + @Get('/search') + @ApiOperation({ + summary: 'Search songs by keywords with pagination and sorting', + }) + public async searchSongs( + @Query() query: PageQueryDTO, + @Query('q') q: string, + ): Promise { + return await this.songService.searchSongs(query, q ?? ''); + } + @Get('/:id') @ApiOperation({ summary: 'Get song info by ID' }) public async getSong( diff --git a/apps/backend/src/song/song.service.ts b/apps/backend/src/song/song.service.ts index 1da06c08..fc6e33d9 100644 --- a/apps/backend/src/song/song.service.ts +++ b/apps/backend/src/song/song.service.ts @@ -202,6 +202,48 @@ export class SongService { }) .skip(page * limit - limit) .limit(limit) + .populate('uploader', 'username publicName profileImage -_id') + .exec()) as unknown as SongWithUser[]; + + return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song)); + } + + public async searchSongs( + query: PageQueryDTO, + q: string, + ): Promise { + const page = parseInt(query.page?.toString() ?? '1'); + const limit = parseInt(query.limit?.toString() ?? '10'); + const order = query.order ? query.order : false; + const allowedSorts = new Set(['likeCount', 'createdAt', 'playCount']); + const sortField = allowedSorts.has(query.sort ?? '') + ? (query.sort as string) + : 'createdAt'; + + const terms = (q || '') + .split(/\s+/) + .map((t) => t.trim()) + .filter((t) => t.length > 0); + + // Build Google-like search: all words must appear across any of the fields + const andClauses = terms.map((word) => ({ + $or: [ + { title: { $regex: word, $options: 'i' } }, + { originalAuthor: { $regex: word, $options: 'i' } }, + { description: { $regex: word, $options: 'i' } }, + ], + })); + + const mongoQuery: any = { + visibility: 'public', + ...(andClauses.length > 0 ? { $and: andClauses } : {}), + }; + + const songs = (await this.songModel + .find(mongoQuery) + .sort({ [sortField]: order ? 1 : -1 }) + .skip(limit * (page - 1)) + .limit(limit) .populate('uploader', 'username profileImage -_id') .exec()) as unknown as SongWithUser[]; diff --git a/packages/database/src/song/dto/SongView.dto.ts b/packages/database/src/song/dto/SongView.dto.ts index b7b5ed6f..b10560c4 100644 --- a/packages/database/src/song/dto/SongView.dto.ts +++ b/packages/database/src/song/dto/SongView.dto.ts @@ -14,6 +14,7 @@ import type { CategoryType, LicenseType, VisibilityType } from './types'; export type SongViewUploader = { username: string; + publicName: string; profileImage: string; }; From 58eb0cdbb587b96c848070e1ad1408191f088ebd Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Thu, 18 Sep 2025 21:29:52 -0300 Subject: [PATCH 02/32] refactor: update Swagger documentation route from '/api/doc' to '/docs' for improved clarity --- apps/backend/src/lib/initializeSwagger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/lib/initializeSwagger.ts b/apps/backend/src/lib/initializeSwagger.ts index ed3c8ef1..2e45498c 100644 --- a/apps/backend/src/lib/initializeSwagger.ts +++ b/apps/backend/src/lib/initializeSwagger.ts @@ -21,5 +21,5 @@ export function initializeSwagger(app: INestApplication) { }, }; - SwaggerModule.setup('api/doc', app, document, swaggerOptions); + SwaggerModule.setup('docs', app, document, swaggerOptions); } From 730ffddd0a5fef950087e8840d51ec03c6ab9f35 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Thu, 18 Sep 2025 21:44:07 -0300 Subject: [PATCH 03/32] feat: add SearchBox component to Header for enhanced song and user search functionality --- .../shared/components/layout/Header.tsx | 2 + .../shared/components/layout/SearchBox.tsx | 52 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 apps/frontend/src/modules/shared/components/layout/SearchBox.tsx diff --git a/apps/frontend/src/modules/shared/components/layout/Header.tsx b/apps/frontend/src/modules/shared/components/layout/Header.tsx index c2862739..c64a6390 100644 --- a/apps/frontend/src/modules/shared/components/layout/Header.tsx +++ b/apps/frontend/src/modules/shared/components/layout/Header.tsx @@ -13,6 +13,7 @@ import { checkLogin, getUserData } from '@web/modules/auth/features/auth.utils'; import { BlockTab } from './BlockTab'; import { NavLinks } from './NavLinks'; import { RandomSongButton } from './RandomSongButton'; +import { SearchBox } from './SearchBox'; export async function Header() { let isLogged; @@ -93,6 +94,7 @@ export async function Header() { label='About' className='bg-cyan-700 after:bg-cyan-900 before:bg-cyan-950' /> + diff --git a/apps/frontend/src/modules/shared/components/layout/SearchBox.tsx b/apps/frontend/src/modules/shared/components/layout/SearchBox.tsx new file mode 100644 index 00000000..1b71565d --- /dev/null +++ b/apps/frontend/src/modules/shared/components/layout/SearchBox.tsx @@ -0,0 +1,52 @@ +'use client'; +import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +import { cn } from '@web/lib/tailwind.utils'; +import { Popover, PopoverContent, PopoverTrigger } from './popover'; + +export const SearchBox = () => { + const [query, setQuery] = useState(''); + const router = useRouter(); + + return ( + + + + Search + + +
+ setQuery(e.target.value)} + placeholder='Search for songs and users' + className='w-full bg-zinc-800 text-white border border-zinc-600 rounded-md p-2' + /> + +
+
+
+ ); +}; From fc3b3cea2b28c44a036f0a87bc70723160201162 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Thu, 18 Sep 2025 21:49:53 -0300 Subject: [PATCH 04/32] fix: update SearchBox component to refine search functionality by changing placeholder text and adjusting query parameter for song searches --- .../src/modules/shared/components/layout/SearchBox.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/modules/shared/components/layout/SearchBox.tsx b/apps/frontend/src/modules/shared/components/layout/SearchBox.tsx index 1b71565d..109b0a3f 100644 --- a/apps/frontend/src/modules/shared/components/layout/SearchBox.tsx +++ b/apps/frontend/src/modules/shared/components/layout/SearchBox.tsx @@ -28,7 +28,7 @@ export const SearchBox = () => { type='text' value={query} onChange={(e) => setQuery(e.target.value)} - placeholder='Search for songs and users' + placeholder='Search for songs' className='w-full bg-zinc-800 text-white border border-zinc-600 rounded-md p-2' />