diff --git a/GEMINI.md b/GEMINI.md
new file mode 100644
index 00000000..920189ed
--- /dev/null
+++ b/GEMINI.md
@@ -0,0 +1,29 @@
+# Gemini Workspace Instructions
+
+This document provides instructions for the Gemini AI assistant to effectively interact with the NoteBlockWorld project.
+
+## Package Manager
+
+This project uses **Bun** as the package manager and runtime. Do not use `npm`, `yarn`, or `pnpm`.
+
+- **Installation:** `bun install`
+- **Running scripts:** `bun run `
+- **Adding dependencies:** `bun add `
+- **Running tests:** `bun test`
+
+## Project Structure
+
+This is a TypeScript monorepo managed with Bun workspaces.
+
+- **`apps/`**: Contains the main applications.
+ - **`apps/backend`**: A NestJS application for the server-side logic.
+ - **`apps/frontend`**: A Next.js application for the user interface.
+- **`packages/`**: Contains shared libraries and modules used across the monorepo.
+ - **`packages/api-client`**: Client for communicating with the backend API.
+ - **`packages/configs`**: Shared configurations (e.g., ESLint, Prettier).
+ - **`packages/database`**: Database schemas, queries, and connection logic.
+ - **`packages/song`**: Core logic for handling and manipulating song data.
+ - **`packages/sounds`**: Logic related to fetching and managing sounds.
+ - **`packages/thumbnail`**: A library for generating song thumbnails.
+- **`tests/`**: Contains end-to-end tests, likely using Cypress.
+- **`tsconfig.base.json`**: The base TypeScript configuration for the entire monorepo.
diff --git a/NoteBlockWorld.code-workspace b/NoteBlockWorld.code-workspace
index 0368f60e..b8a0745d 100644
--- a/NoteBlockWorld.code-workspace
+++ b/NoteBlockWorld.code-workspace
@@ -1,9 +1,5 @@
{
"folders": [
- {
- "path": ".",
- "name": "Root"
- },
{
"path": "./apps/backend",
"name": "Backend"
@@ -32,6 +28,10 @@
"path": "./packages/thumbnail",
"name": "thumbnail"
},
+ {
+ "path": ".",
+ "name": "Root"
+ },
],
"settings": {
"window.title": "${dirty}${rootName}${separator}${profileName}${separator}${appName}",
diff --git a/apps/backend/package.json b/apps/backend/package.json
index f3861fb6..0fcad4ad 100644
--- a/apps/backend/package.json
+++ b/apps/backend/package.json
@@ -14,7 +14,7 @@
"start:dev": "bun --watch run src/main.ts",
"start:debug": "bun --watch run src/main.ts",
"start:prod": "node dist/main",
- "lint": "eslint \"src/**/*.ts\" --fix",
+ "lint": "eslint \"src/**/*.ts\" --fix --config ../../eslint.config.ts",
"test": "bun test src/**/*.spec.ts",
"test:watch": "bun test src/**/*.spec.ts --watch",
"test:cov": "bun test src/**/*.spec.ts --coverage",
@@ -22,6 +22,7 @@
"test:e2e": "bun test e2e/**/*.spec.ts"
},
"dependencies": {
+ "@types/bun": "^1.2.10",
"@aws-sdk/client-s3": "3.717.0",
"@aws-sdk/s3-request-presigner": "3.717.0",
"@encode42/nbs.js": "^5.0.2",
@@ -67,6 +68,7 @@
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.15",
+ "@stylistic/eslint-plugin": "^5.4.0",
"@types/bcryptjs": "^2.4.6",
"@types/bun": "^1.2.10",
"@types/express": "^4.17.21",
diff --git a/apps/backend/scripts/build.ts b/apps/backend/scripts/build.ts
index 5417739e..4d261849 100644
--- a/apps/backend/scripts/build.ts
+++ b/apps/backend/scripts/build.ts
@@ -61,11 +61,11 @@ const build = async () => {
const result = await Bun.build({
entrypoints: ['./src/main.ts'],
- outdir: './dist',
- target: 'bun',
- minify: false,
- sourcemap: 'linked',
- external: optionalRequirePackages.filter((pkg) => {
+ outdir : './dist',
+ target : 'bun',
+ minify : false,
+ sourcemap : 'linked',
+ external : optionalRequirePackages.filter((pkg) => {
try {
require(pkg);
return false;
diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts
index b3f2e3bd..40f44a37 100644
--- a/apps/backend/src/app.module.ts
+++ b/apps/backend/src/app.module.ts
@@ -14,20 +14,19 @@ import { ParseTokenPipe } from './lib/parseToken';
import { MailingModule } from './mailing/mailing.module';
import { SeedModule } from './seed/seed.module';
import { SongModule } from './song/song.module';
-import { SongBrowserModule } from './song-browser/song-browser.module';
import { UserModule } from './user/user.module';
@Module({
imports: [
ConfigModule.forRoot({
- isGlobal: true,
+ isGlobal : true,
envFilePath: ['.env.development', '.env.production'],
validate,
}),
//DatabaseModule,
MongooseModule.forRootAsync({
- imports: [ConfigModule],
- inject: [ConfigService],
+ imports : [ConfigModule],
+ inject : [ConfigService],
useFactory: (
configService: ConfigService,
): MongooseModuleFactoryOptions => {
@@ -35,15 +34,15 @@ import { UserModule } from './user/user.module';
Logger.debug(`Connecting to ${url}`);
return {
- uri: url,
+ uri : url,
retryAttempts: 10,
- retryDelay: 3000,
+ retryDelay : 3000,
};
},
}),
// Mailing
MailerModule.forRootAsync({
- imports: [ConfigModule],
+ imports : [ConfigModule],
useFactory: (configService: ConfigService) => {
const transport = configService.getOrThrow('MAIL_TRANSPORT');
const from = configService.getOrThrow('MAIL_FROM');
@@ -51,11 +50,11 @@ import { UserModule } from './user/user.module';
AppModule.logger.debug(`MAIL_FROM: ${from}`);
return {
transport: transport,
- defaults: {
+ defaults : {
from: from,
},
template: {
- dir: __dirname + '/mailing/templates',
+ dir : __dirname + '/mailing/templates',
adapter: new HandlebarsAdapter(),
options: {
strict: true,
@@ -68,7 +67,7 @@ import { UserModule } from './user/user.module';
// Throttler
ThrottlerModule.forRoot([
{
- ttl: 60,
+ ttl : 60,
limit: 256, // 256 requests per minute
},
]),
@@ -76,16 +75,15 @@ import { UserModule } from './user/user.module';
UserModule,
AuthModule.forRootAsync(),
FileModule.forRootAsync(),
- SongBrowserModule,
SeedModule.forRoot(),
EmailLoginModule,
MailingModule,
],
controllers: [],
- providers: [
+ providers : [
ParseTokenPipe,
{
- provide: APP_GUARD,
+ provide : APP_GUARD,
useClass: ThrottlerGuard,
},
],
diff --git a/apps/backend/src/auth/auth.controller.spec.ts b/apps/backend/src/auth/auth.controller.spec.ts
index d6bae4fa..7420acac 100644
--- a/apps/backend/src/auth/auth.controller.spec.ts
+++ b/apps/backend/src/auth/auth.controller.spec.ts
@@ -6,10 +6,10 @@ import { AuthService } from './auth.service';
import { MagicLinkEmailStrategy } from './strategies/magicLinkEmail.strategy';
const mockAuthService = {
- githubLogin: jest.fn(),
- googleLogin: jest.fn(),
- discordLogin: jest.fn(),
- verifyToken: jest.fn(),
+ githubLogin : jest.fn(),
+ googleLogin : jest.fn(),
+ discordLogin : jest.fn(),
+ verifyToken : jest.fn(),
loginWithEmail: jest.fn(),
};
@@ -24,13 +24,13 @@ describe('AuthController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
- providers: [
+ providers : [
{
- provide: AuthService,
+ provide : AuthService,
useValue: mockAuthService,
},
{
- provide: MagicLinkEmailStrategy,
+ provide : MagicLinkEmailStrategy,
useValue: mockMagicLinkEmailStrategy,
},
],
diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts
index 66e29486..482c8423 100644
--- a/apps/backend/src/auth/auth.controller.ts
+++ b/apps/backend/src/auth/auth.controller.ts
@@ -32,7 +32,7 @@ export class AuthController {
@Throttle({
default: {
// one every 1 hour
- ttl: 60 * 60 * 1000,
+ ttl : 60 * 60 * 1000,
limit: 1,
},
})
@@ -44,11 +44,11 @@ export class AuthController {
content: {
'application/json': {
schema: {
- type: 'object',
+ type : 'object',
properties: {
destination: {
- type: 'string',
- example: 'vycasnicolas@gmail.com',
+ type : 'string',
+ example : 'vycasnicolas@gmail.com',
description: 'Email address to send the magic link to',
},
},
@@ -58,7 +58,7 @@ export class AuthController {
},
},
})
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
+
public async magicLinkLogin(@Req() req: Request, @Res() res: Response) {
throw new HttpException('Not implemented', HttpStatus.NOT_IMPLEMENTED);
// TODO: uncomment this line to enable magic link login
diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts
index e266b0bf..c6cfa556 100644
--- a/apps/backend/src/auth/auth.module.ts
+++ b/apps/backend/src/auth/auth.module.ts
@@ -17,14 +17,14 @@ import { MagicLinkEmailStrategy } from './strategies/magicLinkEmail.strategy';
export class AuthModule {
static forRootAsync(): DynamicModule {
return {
- module: AuthModule,
+ module : AuthModule,
imports: [
UserModule,
ConfigModule.forRoot(),
MailingModule,
JwtModule.registerAsync({
- inject: [ConfigService],
- imports: [ConfigModule],
+ inject : [ConfigService],
+ imports : [ConfigModule],
useFactory: async (config: ConfigService) => {
const JWT_SECRET = config.get('JWT_SECRET');
const JWT_EXPIRES_IN = config.get('JWT_EXPIRES_IN');
@@ -39,14 +39,14 @@ export class AuthModule {
}
return {
- secret: JWT_SECRET,
+ secret : JWT_SECRET,
signOptions: { expiresIn: JWT_EXPIRES_IN || '60s' },
};
},
}),
],
controllers: [AuthController],
- providers: [
+ providers : [
AuthService,
ConfigService,
GoogleStrategy,
@@ -55,56 +55,56 @@ export class AuthModule {
MagicLinkEmailStrategy,
JwtStrategy,
{
- inject: [ConfigService],
- provide: 'COOKIE_EXPIRES_IN',
+ inject : [ConfigService],
+ provide : 'COOKIE_EXPIRES_IN',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('COOKIE_EXPIRES_IN'),
},
{
- inject: [ConfigService],
- provide: 'SERVER_URL',
+ inject : [ConfigService],
+ provide : 'SERVER_URL',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('SERVER_URL'),
},
{
- inject: [ConfigService],
- provide: 'FRONTEND_URL',
+ inject : [ConfigService],
+ provide : 'FRONTEND_URL',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('FRONTEND_URL'),
},
{
- inject: [ConfigService],
- provide: 'JWT_SECRET',
+ inject : [ConfigService],
+ provide : 'JWT_SECRET',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('JWT_SECRET'),
},
{
- inject: [ConfigService],
- provide: 'JWT_EXPIRES_IN',
+ inject : [ConfigService],
+ provide : 'JWT_EXPIRES_IN',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('JWT_EXPIRES_IN'),
},
{
- inject: [ConfigService],
- provide: 'JWT_REFRESH_SECRET',
+ inject : [ConfigService],
+ provide : 'JWT_REFRESH_SECRET',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('JWT_REFRESH_SECRET'),
},
{
- inject: [ConfigService],
- provide: 'JWT_REFRESH_EXPIRES_IN',
+ inject : [ConfigService],
+ provide : 'JWT_REFRESH_EXPIRES_IN',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('JWT_REFRESH_EXPIRES_IN'),
},
{
- inject: [ConfigService],
- provide: 'MAGIC_LINK_SECRET',
+ inject : [ConfigService],
+ provide : 'MAGIC_LINK_SECRET',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('MAGIC_LINK_SECRET'),
},
{
- inject: [ConfigService],
- provide: 'APP_DOMAIN',
+ inject : [ConfigService],
+ provide : 'APP_DOMAIN',
useFactory: (configService: ConfigService) =>
configService.get('APP_DOMAIN'),
},
diff --git a/apps/backend/src/auth/auth.service.spec.ts b/apps/backend/src/auth/auth.service.spec.ts
index 311fece9..afcd563f 100644
--- a/apps/backend/src/auth/auth.service.spec.ts
+++ b/apps/backend/src/auth/auth.service.spec.ts
@@ -1,7 +1,8 @@
+import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test';
+
import type { UserDocument } from '@nbw/database';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
-import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test';
import type { Request, Response } from 'express';
import { UserService } from '@server/user/user.service';
@@ -10,9 +11,9 @@ import { AuthService } from './auth.service';
import { Profile } from './types/profile';
const mockAxios = {
- get: jest.fn(),
- post: jest.fn(),
- put: jest.fn(),
+ get : jest.fn(),
+ post : jest.fn(),
+ put : jest.fn(),
delete: jest.fn(),
create: jest.fn(),
};
@@ -21,15 +22,15 @@ mock.module('axios', () => mockAxios);
const mockUserService = {
generateUsername: jest.fn(),
- findByEmail: jest.fn(),
- findByID: jest.fn(),
- create: jest.fn(),
+ findByEmail : jest.fn(),
+ findByID : jest.fn(),
+ create : jest.fn(),
};
const mockJwtService = {
- decode: jest.fn(),
+ decode : jest.fn(),
signAsync: jest.fn(),
- verify: jest.fn(),
+ verify : jest.fn(),
};
describe('AuthService', () => {
@@ -42,47 +43,47 @@ describe('AuthService', () => {
providers: [
AuthService,
{
- provide: UserService,
+ provide : UserService,
useValue: mockUserService,
},
{
- provide: JwtService,
+ provide : JwtService,
useValue: mockJwtService,
},
{
- provide: 'COOKIE_EXPIRES_IN',
+ provide : 'COOKIE_EXPIRES_IN',
useValue: '3600',
},
{
- provide: 'FRONTEND_URL',
+ provide : 'FRONTEND_URL',
useValue: 'http://frontend.test.com',
},
{
- provide: 'COOKIE_EXPIRES_IN',
+ provide : 'COOKIE_EXPIRES_IN',
useValue: '3600',
},
{
- provide: 'JWT_SECRET',
+ provide : 'JWT_SECRET',
useValue: 'test-jwt-secret',
},
{
- provide: 'JWT_EXPIRES_IN',
+ provide : 'JWT_EXPIRES_IN',
useValue: '1d',
},
{
- provide: 'JWT_REFRESH_SECRET',
+ provide : 'JWT_REFRESH_SECRET',
useValue: 'test-jwt-refresh-secret',
},
{
- provide: 'JWT_REFRESH_EXPIRES_IN',
+ provide : 'JWT_REFRESH_EXPIRES_IN',
useValue: '7d',
},
{
- provide: 'WHITELISTED_USERS',
+ provide : 'WHITELISTED_USERS',
useValue: 'tomast1337,bentroen,testuser',
},
{
- provide: 'APP_DOMAIN',
+ provide : 'APP_DOMAIN',
useValue: '.test.com',
},
],
@@ -103,7 +104,7 @@ describe('AuthService', () => {
const res = {
status: jest.fn().mockReturnThis(),
- json: jest.fn(),
+ json : jest.fn(),
} as any;
await authService.verifyToken(req, res);
@@ -120,7 +121,7 @@ describe('AuthService', () => {
const res = {
status: jest.fn().mockReturnThis(),
- json: jest.fn(),
+ json : jest.fn(),
} as any;
await authService.verifyToken(req, res);
@@ -136,7 +137,7 @@ describe('AuthService', () => {
const res = {
status: jest.fn().mockReturnThis(),
- json: jest.fn(),
+ json : jest.fn(),
} as any;
mockJwtService.verify.mockReturnValueOnce({ id: 'test-id' });
@@ -155,7 +156,7 @@ describe('AuthService', () => {
const res = {
status: jest.fn().mockReturnThis(),
- json: jest.fn(),
+ json : jest.fn(),
} as any;
const decodedToken = { id: 'test-id' };
@@ -206,17 +207,17 @@ describe('AuthService', () => {
const tokens = await (authService as any).createJwtPayload(payload);
expect(tokens).toEqual({
- access_token: accessToken,
+ access_token : accessToken,
refresh_token: refreshToken,
});
expect(jwtService.signAsync).toHaveBeenCalledWith(payload, {
- secret: 'test-jwt-secret',
+ secret : 'test-jwt-secret',
expiresIn: '1d',
});
expect(jwtService.signAsync).toHaveBeenCalledWith(payload, {
- secret: 'test-jwt-refresh-secret',
+ secret : 'test-jwt-refresh-secret',
expiresIn: '7d',
});
});
@@ -225,18 +226,18 @@ describe('AuthService', () => {
describe('GenTokenRedirect', () => {
it('should set cookies and redirect to the frontend URL', async () => {
const user_registered = {
- _id: 'user-id',
- email: 'test@example.com',
+ _id : 'user-id',
+ email : 'test@example.com',
username: 'testuser',
} as unknown as UserDocument;
const res = {
- cookie: jest.fn(),
+ cookie : jest.fn(),
redirect: jest.fn(),
} as unknown as Response;
const tokens = {
- access_token: 'access-token',
+ access_token : 'access-token',
refresh_token: 'refresh-token',
};
@@ -245,8 +246,8 @@ describe('AuthService', () => {
await (authService as any).GenTokenRedirect(user_registered, res);
expect((authService as any).createJwtPayload).toHaveBeenCalledWith({
- id: 'user-id',
- email: 'test@example.com',
+ id : 'user-id',
+ email : 'test@example.com',
username: 'testuser',
});
@@ -271,8 +272,8 @@ describe('AuthService', () => {
describe('verifyAndGetUser', () => {
it('should create a new user if the user is not registered', async () => {
const user: Profile = {
- username: 'testuser',
- email: 'test@example.com',
+ username : 'testuser',
+ email : 'test@example.com',
profileImage: 'http://example.com/photo.jpg',
};
@@ -285,7 +286,7 @@ describe('AuthService', () => {
expect(userService.create).toHaveBeenCalledWith(
expect.objectContaining({
- email: 'test@example.com',
+ email : 'test@example.com',
profileImage: 'http://example.com/photo.jpg',
}),
);
@@ -295,13 +296,13 @@ describe('AuthService', () => {
it('should return the registered user if the user is already registered', async () => {
const user: Profile = {
- username: 'testuser',
- email: 'test@example.com',
+ username : 'testuser',
+ email : 'test@example.com',
profileImage: 'http://example.com/photo.jpg',
};
const registeredUser = {
- id: 'registered-user-id',
+ id : 'registered-user-id',
profileImage: 'http://example.com/photo.jpg',
};
@@ -315,15 +316,15 @@ describe('AuthService', () => {
it('should update the profile image if it has changed', async () => {
const user: Profile = {
- username: 'testuser',
- email: 'test@example.com',
+ username : 'testuser',
+ email : 'test@example.com',
profileImage: 'http://example.com/new-photo.jpg',
};
const registeredUser = {
- id: 'registered-user-id',
+ id : 'registered-user-id',
profileImage: 'http://example.com/old-photo.jpg',
- save: jest.fn(),
+ save : jest.fn(),
};
mockUserService.findByEmail.mockResolvedValue(registeredUser);
diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts
index b590e003..4332a6cf 100644
--- a/apps/backend/src/auth/auth.service.ts
+++ b/apps/backend/src/auth/auth.service.ts
@@ -7,6 +7,8 @@ import type { Request, Response } from 'express';
import { UserService } from '@server/user/user.service';
+
+
import { DiscordUser } from './types/discordProfile';
import { GithubAccessToken, GithubEmailList } from './types/githubProfile';
import { GoogleProfile } from './types/googleProfile';
@@ -76,8 +78,8 @@ export class AuthService {
const profile = {
// Generate username from display name
- username: email.split('@')[0],
- email: email,
+ username : email.split('@')[0],
+ email : email,
profileImage: user.photos[0].value,
};
@@ -93,8 +95,8 @@ export class AuthService {
const newUsername = await this.userService.generateUsername(baseUsername);
const newUser = new CreateUser({
- username: newUsername,
- email: email,
+ username : newUsername,
+ email : email,
profileImage: profileImage,
});
@@ -134,8 +136,8 @@ export class AuthService {
const email = response.data.filter((email) => email.primary)[0].email;
const user_registered = await this.verifyAndGetUser({
- username: profile.username,
- email: email,
+ username : profile.username,
+ email : email,
profileImage: profile.photos[0].value,
});
@@ -148,8 +150,8 @@ export class AuthService {
const profile = {
// Generate username from display name
- username: user.username,
- email: user.email,
+ username : user.username,
+ email : user.email,
profileImage: profilePictureUrl,
};
@@ -172,17 +174,17 @@ export class AuthService {
public async createJwtPayload(payload: TokenPayload): Promise {
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
- secret: this.JWT_SECRET,
+ secret : this.JWT_SECRET,
expiresIn: this.JWT_EXPIRES_IN,
}),
this.jwtService.signAsync(payload, {
- secret: this.JWT_REFRESH_SECRET,
+ secret : this.JWT_REFRESH_SECRET,
expiresIn: this.JWT_REFRESH_EXPIRES_IN,
}),
]);
return {
- access_token: accessToken,
+ access_token : accessToken,
refresh_token: refreshToken,
};
}
@@ -192,8 +194,8 @@ export class AuthService {
res: Response>,
): Promise {
const token = await this.createJwtPayload({
- id: user_registered._id.toString(),
- email: user_registered.email,
+ id : user_registered._id.toString(),
+ email : user_registered.email,
username: user_registered.username,
});
diff --git a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts
index 052cae9e..b8d92640 100644
--- a/apps/backend/src/auth/strategies/JWT.strategy.spec.ts
+++ b/apps/backend/src/auth/strategies/JWT.strategy.spec.ts
@@ -13,7 +13,7 @@ describe('JwtStrategy', () => {
providers: [
JwtStrategy,
{
- provide: ConfigService,
+ provide : ConfigService,
useValue: {
getOrThrow: jest.fn().mockReturnValue('test-secret'),
},
diff --git a/apps/backend/src/auth/strategies/JWT.strategy.ts b/apps/backend/src/auth/strategies/JWT.strategy.ts
index 6311d4e4..f0498f57 100644
--- a/apps/backend/src/auth/strategies/JWT.strategy.ts
+++ b/apps/backend/src/auth/strategies/JWT.strategy.ts
@@ -11,8 +11,8 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
const JWT_SECRET = config.getOrThrow('JWT_SECRET');
super({
- jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
- secretOrKey: JWT_SECRET,
+ jwtFromRequest : ExtractJwt.fromAuthHeaderAsBearerToken(),
+ secretOrKey : JWT_SECRET,
passReqToCallback: true,
});
}
diff --git a/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts b/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts
index e075f778..7490f0a0 100644
--- a/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts
+++ b/apps/backend/src/auth/strategies/discord.strategy/Strategy.spec.ts
@@ -10,10 +10,10 @@ describe('DiscordStrategy', () => {
beforeEach(() => {
const config: DiscordStrategyConfig = {
- clientID: 'test-client-id',
+ clientID : 'test-client-id',
clientSecret: 'test-client-secret',
- callbackUrl: 'http://localhost:3000/callback',
- scope: [
+ callbackUrl : 'http://localhost:3000/callback',
+ scope : [
DiscordPermissionScope.Email,
DiscordPermissionScope.Identify,
DiscordPermissionScope.Connections,
@@ -35,11 +35,11 @@ describe('DiscordStrategy', () => {
it('should validate config', async () => {
const config: DiscordStrategyConfig = {
- clientID: 'test-client-id',
+ clientID : 'test-client-id',
clientSecret: 'test-client-secret',
- callbackUrl: 'http://localhost:3000/callback',
- scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify],
- prompt: 'consent',
+ callbackUrl : 'http://localhost:3000/callback',
+ scope : [DiscordPermissionScope.Email, DiscordPermissionScope.Identify],
+ prompt : 'consent',
};
await expect(strategy['validateConfig'](config)).resolves.toBeUndefined();
@@ -71,47 +71,47 @@ describe('DiscordStrategy', () => {
it('should build profile', () => {
const profileData = {
- id: '123',
- username: 'testuser',
- displayName: 'Test User',
- avatar: 'avatar.png',
- banner: 'banner.png',
- email: 'test@example.com',
- verified: true,
- mfa_enabled: true,
+ id : '123',
+ username : 'testuser',
+ displayName : 'Test User',
+ avatar : 'avatar.png',
+ banner : 'banner.png',
+ email : 'test@example.com',
+ verified : true,
+ mfa_enabled : true,
public_flags: 1,
- flags: 1,
- locale: 'en-US',
- global_name: 'testuser#1234',
+ flags : 1,
+ locale : 'en-US',
+ global_name : 'testuser#1234',
premium_type: 1,
- connections: [],
- guilds: [],
+ connections : [],
+ guilds : [],
} as unknown as Profile;
const profile = strategy['buildProfile'](profileData, 'test-access-token');
expect(profile).toMatchObject({
- provider: 'discord',
- id: '123',
- username: 'testuser',
- displayName: 'Test User',
- avatar: 'avatar.png',
- banner: 'banner.png',
- email: 'test@example.com',
- verified: true,
- mfa_enabled: true,
+ provider : 'discord',
+ id : '123',
+ username : 'testuser',
+ displayName : 'Test User',
+ avatar : 'avatar.png',
+ banner : 'banner.png',
+ email : 'test@example.com',
+ verified : true,
+ mfa_enabled : true,
public_flags: 1,
- flags: 1,
- locale: 'en-US',
- global_name: 'testuser#1234',
+ flags : 1,
+ locale : 'en-US',
+ global_name : 'testuser#1234',
premium_type: 1,
- connections: [],
- guilds: [],
+ connections : [],
+ guilds : [],
access_token: 'test-access-token',
- fetchedAt: expect.any(Date),
- createdAt: expect.any(Date),
- _raw: JSON.stringify(profileData),
- _json: profileData,
+ fetchedAt : expect.any(Date),
+ createdAt : expect.any(Date),
+ _raw : JSON.stringify(profileData),
+ _json : profileData,
});
});
@@ -141,9 +141,9 @@ describe('DiscordStrategy', () => {
it('should enrich profile with scopes', async () => {
const profile = {
- id: '123',
+ id : '123',
connections: [],
- guilds: [],
+ guilds : [],
} as unknown as Profile;
const mockFetchScopeData = jest
diff --git a/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts b/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts
index ebc6d905..4f4574e6 100644
--- a/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts
+++ b/apps/backend/src/auth/strategies/discord.strategy/Strategy.ts
@@ -38,10 +38,10 @@ export default class Strategy extends OAuth2Strategy {
public constructor(options: DiscordStrategyConfig, verify: VerifyFunction) {
super(
{
- scopeSeparator: ' ',
+ scopeSeparator : ' ',
...options,
authorizationURL: 'https://discord.com/api/oauth2/authorize',
- tokenURL: 'https://discord.com/api/oauth2/token',
+ tokenURL : 'https://discord.com/api/oauth2/token',
} as OAuth2StrategyOptions,
verify,
);
@@ -148,27 +148,27 @@ export default class Strategy extends OAuth2Strategy {
private buildProfile(data: Profile, accessToken: string): Profile {
const { id } = data;
return {
- provider: 'discord',
- id: id,
- username: data.username,
- displayName: data.displayName,
- avatar: data.avatar,
- banner: data.banner,
- email: data.email,
- verified: data.verified,
- mfa_enabled: data.mfa_enabled,
+ provider : 'discord',
+ id : id,
+ username : data.username,
+ displayName : data.displayName,
+ avatar : data.avatar,
+ banner : data.banner,
+ email : data.email,
+ verified : data.verified,
+ mfa_enabled : data.mfa_enabled,
public_flags: data.public_flags,
- flags: data.flags,
- locale: data.locale,
- global_name: data.global_name,
+ flags : data.flags,
+ locale : data.locale,
+ global_name : data.global_name,
premium_type: data.premium_type,
- connections: data.connections,
- guilds: data.guilds,
+ connections : data.connections,
+ guilds : data.guilds,
access_token: accessToken,
- fetchedAt: new Date(),
- createdAt: this.calculateCreationDate(id),
- _raw: JSON.stringify(data),
- _json: data as unknown as Record,
+ fetchedAt : new Date(),
+ createdAt : this.calculateCreationDate(id),
+ _raw : JSON.stringify(data),
+ _json : data as unknown as Record,
};
}
diff --git a/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts b/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts
index 0dbc8608..ddeb10dc 100644
--- a/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts
+++ b/apps/backend/src/auth/strategies/discord.strategy/discord.strategy.spec.ts
@@ -12,7 +12,7 @@ describe('DiscordStrategy', () => {
providers: [
DiscordStrategy,
{
- provide: ConfigService,
+ provide : ConfigService,
useValue: {
getOrThrow: jest.fn((key: string) => {
switch (key) {
diff --git a/apps/backend/src/auth/strategies/discord.strategy/index.ts b/apps/backend/src/auth/strategies/discord.strategy/index.ts
index 61dc578a..bfdedd04 100644
--- a/apps/backend/src/auth/strategies/discord.strategy/index.ts
+++ b/apps/backend/src/auth/strategies/discord.strategy/index.ts
@@ -22,12 +22,12 @@ export class DiscordStrategy extends PassportStrategy(strategy, 'discord') {
const SERVER_URL = configService.getOrThrow('SERVER_URL');
const config = {
- clientID: DISCORD_CLIENT_ID,
+ clientID : DISCORD_CLIENT_ID,
clientSecret: DISCORD_CLIENT_SECRET,
- callbackUrl: `${SERVER_URL}/api/v1/auth/discord/callback`,
- scope: [DiscordPermissionScope.Email, DiscordPermissionScope.Identify],
- fetchScope: true,
- prompt: 'none',
+ callbackUrl : `${SERVER_URL}/api/v1/auth/discord/callback`,
+ scope : [DiscordPermissionScope.Email, DiscordPermissionScope.Identify],
+ fetchScope : true,
+ prompt : 'none',
};
super(config);
diff --git a/apps/backend/src/auth/strategies/github.strategy.spec.ts b/apps/backend/src/auth/strategies/github.strategy.spec.ts
index c8793e00..db09ecab 100644
--- a/apps/backend/src/auth/strategies/github.strategy.spec.ts
+++ b/apps/backend/src/auth/strategies/github.strategy.spec.ts
@@ -12,7 +12,7 @@ describe('GithubStrategy', () => {
providers: [
GithubStrategy,
{
- provide: ConfigService,
+ provide : ConfigService,
useValue: {
getOrThrow: jest.fn((key: string) => {
switch (key) {
diff --git a/apps/backend/src/auth/strategies/github.strategy.ts b/apps/backend/src/auth/strategies/github.strategy.ts
index 27293151..d0740048 100644
--- a/apps/backend/src/auth/strategies/github.strategy.ts
+++ b/apps/backend/src/auth/strategies/github.strategy.ts
@@ -20,11 +20,11 @@ export class GithubStrategy extends PassportStrategy(strategy, 'github') {
const SERVER_URL = configService.getOrThrow('SERVER_URL');
super({
- clientID: GITHUB_CLIENT_ID,
+ clientID : GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
redirect_uri: `${SERVER_URL}/api/v1/auth/github/callback`,
- scope: 'user:read,user:email',
- state: false,
+ scope : 'user:read,user:email',
+ state : false,
});
}
diff --git a/apps/backend/src/auth/strategies/google.strategy.spec.ts b/apps/backend/src/auth/strategies/google.strategy.spec.ts
index c1f1233e..8adf94be 100644
--- a/apps/backend/src/auth/strategies/google.strategy.spec.ts
+++ b/apps/backend/src/auth/strategies/google.strategy.spec.ts
@@ -13,7 +13,7 @@ describe('GoogleStrategy', () => {
providers: [
GoogleStrategy,
{
- provide: ConfigService,
+ provide : ConfigService,
useValue: {
getOrThrow: jest.fn((key: string) => {
switch (key) {
diff --git a/apps/backend/src/auth/strategies/google.strategy.ts b/apps/backend/src/auth/strategies/google.strategy.ts
index a19e1789..859b1851 100644
--- a/apps/backend/src/auth/strategies/google.strategy.ts
+++ b/apps/backend/src/auth/strategies/google.strategy.ts
@@ -23,10 +23,10 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
GoogleStrategy.logger.debug(`Google Login callbackURL ${callbackURL}`);
super({
- clientID: GOOGLE_CLIENT_ID,
+ clientID : GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
- callbackURL: callbackURL,
- scope: ['email', 'profile'],
+ callbackURL : callbackURL,
+ scope : ['email', 'profile'],
});
}
diff --git a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts
index a77f26c6..cb5a2454 100644
--- a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts
+++ b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.spec.ts
@@ -13,7 +13,7 @@ describe('MagicLinkEmailStrategy', () => {
let _configService: ConfigService;
const mockUserService = {
- findByEmail: jest.fn(),
+ findByEmail : jest.fn(),
createWithEmail: jest.fn(),
};
@@ -37,11 +37,11 @@ describe('MagicLinkEmailStrategy', () => {
{ provide: MailingService, useValue: mockMailingService },
{ provide: ConfigService, useValue: mockConfigService },
{
- provide: 'MAGIC_LINK_SECRET',
+ provide : 'MAGIC_LINK_SECRET',
useValue: 'test_secret',
},
{
- provide: 'SERVER_URL',
+ provide : 'SERVER_URL',
useValue: 'http://localhost:3000',
},
],
@@ -77,13 +77,13 @@ describe('MagicLinkEmailStrategy', () => {
expect(mockUserService.findByEmail).toHaveBeenCalledWith(email);
expect(mockMailingService.sendEmail).toHaveBeenCalledWith({
- to: email,
+ to : email,
context: {
magicLink:
'http://localhost/api/v1/auth/magic-link/callback?token=test_token',
username: 'testuser',
},
- subject: 'Noteblock Magic Link',
+ subject : 'Noteblock Magic Link',
template: 'magic-link',
});
});
@@ -108,13 +108,13 @@ describe('MagicLinkEmailStrategy', () => {
expect(mockUserService.findByEmail).toHaveBeenCalledWith(email);
expect(mockMailingService.sendEmail).toHaveBeenCalledWith({
- to: email,
+ to : email,
context: {
magicLink:
'http://localhost/api/v1/auth/magic-link/callback?token=test_token',
username: 'testuser',
},
- subject: 'Welcome to Noteblock.world',
+ subject : 'Welcome to Noteblock.world',
template: 'magic-link-new-account',
});
});
@@ -138,14 +138,14 @@ describe('MagicLinkEmailStrategy', () => {
mockUserService.findByEmail.mockResolvedValue(null);
mockUserService.createWithEmail.mockResolvedValue({
- email: 'test@example.com',
+ email : 'test@example.com',
username: 'test',
});
const result = await strategy.validate(payload);
expect(result).toEqual({
- email: 'test@example.com',
+ email : 'test@example.com',
username: 'test',
});
});
diff --git a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts
index eb528158..f819170b 100644
--- a/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts
+++ b/apps/backend/src/auth/strategies/magicLinkEmail.strategy.ts
@@ -29,9 +29,9 @@ export class MagicLinkEmailStrategy extends PassportStrategy(
private readonly mailingService: MailingService,
) {
super({
- secret: MAGIC_LINK_SECRET,
- confirmUrl: `${SERVER_URL}/api/v1/auth/magic-link/confirm`,
- callbackUrl: `${SERVER_URL}/api/v1/auth/magic-link/callback`,
+ secret : MAGIC_LINK_SECRET,
+ confirmUrl : `${SERVER_URL}/api/v1/auth/magic-link/confirm`,
+ callbackUrl : `${SERVER_URL}/api/v1/auth/magic-link/callback`,
sendMagicLink: MagicLinkEmailStrategy.sendMagicLink(
SERVER_URL,
userService,
@@ -57,22 +57,22 @@ export class MagicLinkEmailStrategy extends PassportStrategy(
if (!user) {
mailingService.sendEmail({
- to: email,
+ to : email,
context: {
magicLink: magicLink,
- username: email.split('@')[0],
+ username : email.split('@')[0],
},
- subject: 'Welcome to Noteblock.world',
+ subject : 'Welcome to Noteblock.world',
template: 'magic-link-new-account',
});
} else {
mailingService.sendEmail({
- to: email,
+ to : email,
context: {
magicLink: magicLink,
- username: user.username,
+ username : user.username,
},
- subject: 'Noteblock Magic Link',
+ subject : 'Noteblock Magic Link',
template: 'magic-link',
});
}
diff --git a/apps/backend/src/email-login/email-login.controller.spec.ts b/apps/backend/src/email-login/email-login.controller.spec.ts
index 1e8e48a3..c352b8c9 100644
--- a/apps/backend/src/email-login/email-login.controller.spec.ts
+++ b/apps/backend/src/email-login/email-login.controller.spec.ts
@@ -9,7 +9,7 @@ describe('EmailLoginController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EmailLoginController],
- providers: [EmailLoginService],
+ providers : [EmailLoginService],
}).compile();
controller = module.get(EmailLoginController);
diff --git a/apps/backend/src/email-login/email-login.module.ts b/apps/backend/src/email-login/email-login.module.ts
index 47414fa8..a1de52d7 100644
--- a/apps/backend/src/email-login/email-login.module.ts
+++ b/apps/backend/src/email-login/email-login.module.ts
@@ -5,6 +5,6 @@ import { EmailLoginService } from './email-login.service';
@Module({
controllers: [EmailLoginController],
- providers: [EmailLoginService],
+ providers : [EmailLoginService],
})
export class EmailLoginModule {}
diff --git a/apps/backend/src/file/file.module.ts b/apps/backend/src/file/file.module.ts
index 33303b61..17365459 100644
--- a/apps/backend/src/file/file.module.ts
+++ b/apps/backend/src/file/file.module.ts
@@ -7,42 +7,42 @@ import { FileService } from './file.service';
export class FileModule {
static forRootAsync(): DynamicModule {
return {
- module: FileModule,
- imports: [ConfigModule.forRoot()],
+ module : FileModule,
+ imports : [ConfigModule.forRoot()],
providers: [
{
- provide: 'S3_BUCKET_SONGS',
+ provide : 'S3_BUCKET_SONGS',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('S3_BUCKET_SONGS'),
inject: [ConfigService],
},
{
- provide: 'S3_BUCKET_THUMBS',
+ provide : 'S3_BUCKET_THUMBS',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('S3_BUCKET_THUMBS'),
inject: [ConfigService],
},
{
- provide: 'S3_KEY',
+ provide : 'S3_KEY',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('S3_KEY'),
inject: [ConfigService],
},
{
- provide: 'S3_SECRET',
+ provide : 'S3_SECRET',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('S3_SECRET'),
inject: [ConfigService],
},
{
- provide: 'S3_ENDPOINT',
+ provide : 'S3_ENDPOINT',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('S3_ENDPOINT'),
inject: [ConfigService],
},
{
- provide: 'S3_REGION',
+ provide : 'S3_REGION',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('S3_REGION'),
inject: [ConfigService],
diff --git a/apps/backend/src/file/file.service.spec.ts b/apps/backend/src/file/file.service.spec.ts
index 8d5e9e64..5e7642f8 100644
--- a/apps/backend/src/file/file.service.spec.ts
+++ b/apps/backend/src/file/file.service.spec.ts
@@ -1,7 +1,8 @@
+import { beforeEach, describe, expect, it, jest, mock } from 'bun:test';
+
import { S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Test, TestingModule } from '@nestjs/testing';
-import { beforeEach, describe, expect, it, jest, mock } from 'bun:test';
import { FileService } from './file.service';
@@ -11,12 +12,12 @@ mock.module('@aws-sdk/client-s3', () => {
};
return {
- S3Client: jest.fn(() => mS3Client),
- GetObjectCommand: jest.fn(),
- PutObjectCommand: jest.fn(),
+ S3Client : jest.fn(() => mS3Client),
+ GetObjectCommand : jest.fn(),
+ PutObjectCommand : jest.fn(),
HeadBucketCommand: jest.fn(),
- ObjectCannedACL: {
- private: 'private',
+ ObjectCannedACL : {
+ private : 'private',
public_read: 'public-read',
},
};
@@ -35,27 +36,27 @@ describe('FileService', () => {
providers: [
FileService,
{
- provide: 'S3_BUCKET_THUMBS',
+ provide : 'S3_BUCKET_THUMBS',
useValue: 'test-bucket-thumbs',
},
{
- provide: 'S3_BUCKET_SONGS',
+ provide : 'S3_BUCKET_SONGS',
useValue: 'test-bucket-songs',
},
{
- provide: 'S3_KEY',
+ provide : 'S3_KEY',
useValue: 'test-key',
},
{
- provide: 'S3_SECRET',
+ provide : 'S3_SECRET',
useValue: 'test-secret',
},
{
- provide: 'S3_ENDPOINT',
+ provide : 'S3_ENDPOINT',
useValue: 'test-endpoint',
},
{
- provide: 'S3_REGION',
+ provide : 'S3_REGION',
useValue: 'test-region',
},
],
diff --git a/apps/backend/src/file/file.service.ts b/apps/backend/src/file/file.service.ts
index 1eda743e..f9160149 100644
--- a/apps/backend/src/file/file.service.ts
+++ b/apps/backend/src/file/file.service.ts
@@ -74,10 +74,10 @@ export class FileService {
// Create S3 client
const s3Client = new S3Client({
- region: region,
- endpoint: endpoint,
+ region : region,
+ endpoint : endpoint,
credentials: {
- accessKeyId: key,
+ accessKeyId : key,
secretAccessKey: secret,
},
forcePathStyle: endpoint.includes('localhost') ? true : false,
@@ -129,8 +129,8 @@ export class FileService {
const bucket = this.S3_BUCKET_SONGS;
const command = new GetObjectCommand({
- Bucket: bucket,
- Key: key,
+ Bucket : bucket,
+ Key : key,
ResponseContentDisposition: `attachment; filename="${filename.replace(
/[/"]/g,
'_',
@@ -183,7 +183,7 @@ export class FileService {
const command = new GetObjectCommand({
Bucket: bucket,
- Key: nbsFileUrl,
+ Key : nbsFileUrl,
});
try {
@@ -204,12 +204,12 @@ export class FileService {
accessControl: ObjectCannedACL = ObjectCannedACL.public_read,
) {
const params = {
- Bucket: bucket,
- Key: String(name),
- Body: file,
- ACL: accessControl,
- ContentType: mimetype,
- ContentDisposition: `attachment; filename=${name.split('/').pop()}`,
+ Bucket : bucket,
+ Key : String(name),
+ Body : file,
+ ACL : accessControl,
+ ContentType : mimetype,
+ ContentDisposition : `attachment; filename=${name.split('/').pop()}`,
CreateBucketConfiguration: {
LocationConstraint: 'ap-south-1',
},
@@ -231,7 +231,7 @@ export class FileService {
const command = new GetObjectCommand({
Bucket: bucket,
- Key: nbsFileUrl,
+ Key : nbsFileUrl,
});
try {
diff --git a/apps/backend/src/lib/GetRequestUser.spec.ts b/apps/backend/src/lib/GetRequestUser.spec.ts
index ebc2e65f..35c27c4a 100644
--- a/apps/backend/src/lib/GetRequestUser.spec.ts
+++ b/apps/backend/src/lib/GetRequestUser.spec.ts
@@ -18,7 +18,7 @@ describe('GetRequestToken', () => {
describe('validateUser', () => {
it('should return the user if the user exists', () => {
const mockUser = {
- _id: 'test-id',
+ _id : 'test-id',
username: 'testuser',
} as unknown as UserDocument;
diff --git a/apps/backend/src/lib/initializeSwagger.spec.ts b/apps/backend/src/lib/initializeSwagger.spec.ts
index 8792226b..ef396009 100644
--- a/apps/backend/src/lib/initializeSwagger.spec.ts
+++ b/apps/backend/src/lib/initializeSwagger.spec.ts
@@ -1,20 +1,21 @@
+import { beforeEach, describe, expect, it, jest, mock } from 'bun:test';
+
import { INestApplication } from '@nestjs/common';
import { SwaggerModule } from '@nestjs/swagger';
-import { beforeEach, describe, expect, it, jest, mock } from 'bun:test';
import { initializeSwagger } from './initializeSwagger';
mock.module('@nestjs/swagger', () => ({
DocumentBuilder: jest.fn().mockImplementation(() => ({
- setTitle: jest.fn().mockReturnThis(),
+ setTitle : jest.fn().mockReturnThis(),
setDescription: jest.fn().mockReturnThis(),
- setVersion: jest.fn().mockReturnThis(),
- addBearerAuth: jest.fn().mockReturnThis(),
- build: jest.fn().mockReturnValue({}),
+ setVersion : jest.fn().mockReturnThis(),
+ addBearerAuth : jest.fn().mockReturnThis(),
+ build : jest.fn().mockReturnValue({}),
})),
SwaggerModule: {
createDocument: jest.fn().mockReturnValue({}),
- setup: jest.fn(),
+ setup : jest.fn(),
},
}));
@@ -34,7 +35,7 @@ describe('initializeSwagger', () => {
);
expect(SwaggerModule.setup).toHaveBeenCalledWith(
- 'api/doc',
+ 'docs',
app,
expect.any(Object),
{
diff --git a/apps/backend/src/lib/parseToken.spec.ts b/apps/backend/src/lib/parseToken.spec.ts
index 8db755f3..9d785915 100644
--- a/apps/backend/src/lib/parseToken.spec.ts
+++ b/apps/backend/src/lib/parseToken.spec.ts
@@ -14,7 +14,7 @@ describe('ParseTokenPipe', () => {
providers: [
ParseTokenPipe,
{
- provide: AuthService,
+ provide : AuthService,
useValue: {
getUserFromToken: jest.fn(),
},
@@ -34,7 +34,7 @@ describe('ParseTokenPipe', () => {
it('should return true if no authorization header is present', async () => {
const mockExecutionContext = {
switchToHttp: jest.fn().mockReturnThis(),
- getRequest: jest.fn().mockReturnValue({ headers: {} }),
+ getRequest : jest.fn().mockReturnValue({ headers: {} }),
} as unknown as ExecutionContext;
const result = await parseTokenPipe.canActivate(mockExecutionContext);
@@ -45,7 +45,7 @@ describe('ParseTokenPipe', () => {
it('should return true if user is not found from token', async () => {
const mockExecutionContext = {
switchToHttp: jest.fn().mockReturnThis(),
- getRequest: jest.fn().mockReturnValue({
+ getRequest : jest.fn().mockReturnValue({
headers: { authorization: 'Bearer test-token' },
}),
} as unknown as ExecutionContext;
@@ -63,8 +63,8 @@ describe('ParseTokenPipe', () => {
const mockExecutionContext = {
switchToHttp: jest.fn().mockReturnThis(),
- getRequest: jest.fn().mockReturnValue({
- headers: { authorization: 'Bearer test-token' },
+ getRequest : jest.fn().mockReturnValue({
+ headers : { authorization: 'Bearer test-token' },
existingUser: null,
}),
} as unknown as ExecutionContext;
diff --git a/apps/backend/src/mailing/mailing.controller.spec.ts b/apps/backend/src/mailing/mailing.controller.spec.ts
index cebfd371..1e1633c0 100644
--- a/apps/backend/src/mailing/mailing.controller.spec.ts
+++ b/apps/backend/src/mailing/mailing.controller.spec.ts
@@ -9,9 +9,9 @@ describe('MailingController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MailingController],
- providers: [
+ providers : [
{
- provide: MailingService,
+ provide : MailingService,
useValue: {},
},
],
diff --git a/apps/backend/src/mailing/mailing.module.ts b/apps/backend/src/mailing/mailing.module.ts
index fb737e0e..e710ad3e 100644
--- a/apps/backend/src/mailing/mailing.module.ts
+++ b/apps/backend/src/mailing/mailing.module.ts
@@ -5,7 +5,7 @@ import { MailingService } from './mailing.service';
@Module({
controllers: [MailingController],
- providers: [MailingService],
- exports: [MailingService],
+ providers : [MailingService],
+ exports : [MailingService],
})
export class MailingModule {}
diff --git a/apps/backend/src/mailing/mailing.service.spec.ts b/apps/backend/src/mailing/mailing.service.spec.ts
index 4918150e..6d29f30c 100644
--- a/apps/backend/src/mailing/mailing.service.spec.ts
+++ b/apps/backend/src/mailing/mailing.service.spec.ts
@@ -16,7 +16,7 @@ describe('MailingService', () => {
providers: [
MailingService,
{
- provide: MailerService,
+ provide : MailerService,
useValue: MockedMailerService,
},
],
@@ -40,7 +40,7 @@ describe('MailingService', () => {
const template = 'hello';
const context = {
- name: 'John Doe',
+ name : 'John Doe',
message: 'Hello, this is a test email!',
};
@@ -54,13 +54,13 @@ describe('MailingService', () => {
attachments: [
{
filename: 'background-image.png',
- cid: 'background-image',
- path: `${__dirname}/templates/img/background-image.png`,
+ cid : 'background-image',
+ path : `${__dirname}/templates/img/background-image.png`,
},
{
filename: 'logo.png',
- cid: 'logo',
- path: `${__dirname}/templates/img/logo.png`,
+ cid : 'logo',
+ path : `${__dirname}/templates/img/logo.png`,
},
],
});
diff --git a/apps/backend/src/mailing/mailing.service.ts b/apps/backend/src/mailing/mailing.service.ts
index 8fd5b447..41c919df 100644
--- a/apps/backend/src/mailing/mailing.service.ts
+++ b/apps/backend/src/mailing/mailing.service.ts
@@ -28,18 +28,18 @@ export class MailingService {
await this.mailerService.sendMail({
to,
subject,
- template: `${template}`, // The template file name (without extension)
+ template : `${template}`, // The template file name (without extension)
context, // The context to be passed to the template
attachments: [
{
filename: 'background-image.png',
- cid: 'background-image',
- path: `${__dirname}/templates/img/background-image.png`,
+ cid : 'background-image',
+ path : `${__dirname}/templates/img/background-image.png`,
},
{
filename: 'logo.png',
- cid: 'logo',
- path: `${__dirname}/templates/img/logo.png`,
+ cid : 'logo',
+ path : `${__dirname}/templates/img/logo.png`,
},
],
});
diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts
index 389ee3c0..bb562f51 100644
--- a/apps/backend/src/main.ts
+++ b/apps/backend/src/main.ts
@@ -18,7 +18,7 @@ async function bootstrap() {
app.useGlobalPipes(
new ValidationPipe({
- transform: true,
+ transform : true,
transformOptions: {
enableImplicitConversion: true,
},
@@ -36,8 +36,8 @@ async function bootstrap() {
app.enableCors({
allowedHeaders: ['content-type', 'authorization', 'src'],
exposedHeaders: ['Content-Disposition'],
- origin: [process.env.FRONTEND_URL || '', 'https://bentroen.github.io'],
- credentials: true,
+ origin : [process.env.FRONTEND_URL || '', 'https://bentroen.github.io'],
+ credentials : true,
});
app.use('/v1', express.static('public'));
diff --git a/apps/backend/src/seed/seed.controller.spec.ts b/apps/backend/src/seed/seed.controller.spec.ts
index 63cc91ea..9c158e5b 100644
--- a/apps/backend/src/seed/seed.controller.spec.ts
+++ b/apps/backend/src/seed/seed.controller.spec.ts
@@ -9,9 +9,9 @@ describe('SeedController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SeedController],
- providers: [
+ providers : [
{
- provide: SeedService,
+ provide : SeedService,
useValue: {},
},
],
diff --git a/apps/backend/src/seed/seed.module.ts b/apps/backend/src/seed/seed.module.ts
index bf4ded79..b3123f67 100644
--- a/apps/backend/src/seed/seed.module.ts
+++ b/apps/backend/src/seed/seed.module.ts
@@ -21,13 +21,13 @@ export class SeedModule {
} else {
SeedModule.logger.warn('Seeding is allowed in development mode');
return {
- module: SeedModule,
- imports: [UserModule, SongModule, ConfigModule.forRoot()],
+ module : SeedModule,
+ imports : [UserModule, SongModule, ConfigModule.forRoot()],
providers: [
ConfigService,
SeedService,
{
- provide: 'NODE_ENV',
+ provide : 'NODE_ENV',
useFactory: (configService: ConfigService) =>
configService.get('NODE_ENV'),
inject: [ConfigService],
diff --git a/apps/backend/src/seed/seed.service.spec.ts b/apps/backend/src/seed/seed.service.spec.ts
index 7577e9c4..c894af4c 100644
--- a/apps/backend/src/seed/seed.service.spec.ts
+++ b/apps/backend/src/seed/seed.service.spec.ts
@@ -13,19 +13,19 @@ describe('SeedService', () => {
providers: [
SeedService,
{
- provide: 'NODE_ENV',
+ provide : 'NODE_ENV',
useValue: 'development',
},
{
- provide: UserService,
+ provide : UserService,
useValue: {
createWithPassword: jest.fn(),
},
},
{
- provide: SongService,
+ provide : SongService,
useValue: {
- uploadSong: jest.fn(),
+ uploadSong : jest.fn(),
getSongById: jest.fn(),
},
},
diff --git a/apps/backend/src/seed/seed.service.ts b/apps/backend/src/seed/seed.service.ts
index af3798eb..2f279d7f 100644
--- a/apps/backend/src/seed/seed.service.ts
+++ b/apps/backend/src/seed/seed.service.ts
@@ -65,21 +65,21 @@ export class SeedService {
user.description = faker.lorem.paragraph();
user.socialLinks = {
- youtube: faker.internet.url(),
- x: faker.internet.url(),
- discord: faker.internet.url(),
- instagram: faker.internet.url(),
- twitch: faker.internet.url(),
- bandcamp: faker.internet.url(),
- facebook: faker.internet.url(),
- github: faker.internet.url(),
- reddit: faker.internet.url(),
- snapchat: faker.internet.url(),
+ youtube : faker.internet.url(),
+ x : faker.internet.url(),
+ discord : faker.internet.url(),
+ instagram : faker.internet.url(),
+ twitch : faker.internet.url(),
+ bandcamp : faker.internet.url(),
+ facebook : faker.internet.url(),
+ github : faker.internet.url(),
+ reddit : faker.internet.url(),
+ snapchat : faker.internet.url(),
soundcloud: faker.internet.url(),
- spotify: faker.internet.url(),
- steam: faker.internet.url(),
- telegram: faker.internet.url(),
- tiktok: faker.internet.url(),
+ spotify : faker.internet.url(),
+ steam : faker.internet.url(),
+ telegram : faker.internet.url(),
+ tiktok : faker.internet.url(),
};
// remove some social links randomly to simulate users not having all of them or having none
@@ -109,26 +109,26 @@ export class SeedService {
const body: UploadSongDto = {
file: {
- buffer: fileData,
- size: fileBuffer.length,
- mimetype: 'application/octet-stream',
+ buffer : fileData,
+ size : fileBuffer.length,
+ mimetype : 'application/octet-stream',
originalname: `${faker.music.songName()}.nbs`,
},
allowDownload: faker.datatype.boolean(),
- visibility: faker.helpers.arrayElement(
+ visibility : faker.helpers.arrayElement(
visibilities,
) as VisibilityType,
- title: faker.music.songName(),
- originalAuthor: faker.music.artist(),
- description: faker.lorem.paragraph(),
- license: faker.helpers.arrayElement(licenses) as LicenseType,
- category: faker.helpers.arrayElement(categories) as CategoryType,
+ title : faker.music.songName(),
+ originalAuthor : faker.music.artist(),
+ description : faker.lorem.paragraph(),
+ license : faker.helpers.arrayElement(licenses) as LicenseType,
+ category : faker.helpers.arrayElement(categories) as CategoryType,
customInstruments: [],
- thumbnailData: {
+ thumbnailData : {
backgroundColor: faker.internet.color(),
- startLayer: faker.helpers.rangeToNumber({ min: 0, max: 4 }),
- startTick: faker.helpers.rangeToNumber({ min: 0, max: 100 }),
- zoomLevel: faker.helpers.rangeToNumber({ min: 1, max: 5 }),
+ startLayer : faker.helpers.rangeToNumber({ min: 0, max: 4 }),
+ startTick : faker.helpers.rangeToNumber({ min: 0, max: 100 }),
+ zoomLevel : faker.helpers.rangeToNumber({ min: 1, max: 5 }),
},
};
@@ -197,10 +197,10 @@ export class SeedService {
}).map(
() =>
new Note(instrument, {
- key: faker.helpers.rangeToNumber({ min: 0, max: 127 }),
+ key : faker.helpers.rangeToNumber({ min: 0, max: 127 }),
velocity: faker.helpers.rangeToNumber({ min: 0, max: 127 }),
- panning: faker.helpers.rangeToNumber({ min: -1, max: 1 }),
- pitch: faker.helpers.rangeToNumber({ min: -1, max: 1 }),
+ panning : faker.helpers.rangeToNumber({ min: -1, max: 1 }),
+ pitch : faker.helpers.rangeToNumber({ min: -1, max: 1 }),
}),
);
@@ -216,7 +216,7 @@ export class SeedService {
return new Date(
faker.date.between({
from: from.getTime(),
- to: to.getTime(),
+ to : to.getTime(),
}),
);
}
diff --git a/apps/backend/src/song-browser/song-browser.controller.spec.ts b/apps/backend/src/song-browser/song-browser.controller.spec.ts
deleted file mode 100644
index 0e95d2ff..00000000
--- a/apps/backend/src/song-browser/song-browser.controller.spec.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { FeaturedSongsDto, PageQueryDTO, SongPreviewDto } from '@nbw/database';
-import { Test, TestingModule } from '@nestjs/testing';
-
-import { SongBrowserController } from './song-browser.controller';
-import { SongBrowserService } from './song-browser.service';
-
-const mockSongBrowserService = {
- getFeaturedSongs: jest.fn(),
- getRecentSongs: jest.fn(),
- getCategories: jest.fn(),
- getSongsByCategory: jest.fn(),
-};
-
-describe('SongBrowserController', () => {
- let controller: SongBrowserController;
- let songBrowserService: SongBrowserService;
-
- beforeEach(async () => {
- const module: TestingModule = await Test.createTestingModule({
- controllers: [SongBrowserController],
- providers: [
- {
- provide: SongBrowserService,
- useValue: mockSongBrowserService,
- },
- ],
- }).compile();
-
- controller = module.get(SongBrowserController);
- songBrowserService = module.get(SongBrowserService);
- });
-
- it('should be defined', () => {
- expect(controller).toBeDefined();
- });
-
- describe('getFeaturedSongs', () => {
- it('should return a list of featured songs', async () => {
- const featuredSongs: FeaturedSongsDto = {} as FeaturedSongsDto;
-
- mockSongBrowserService.getFeaturedSongs.mockResolvedValueOnce(
- featuredSongs,
- );
-
- const result = await controller.getFeaturedSongs();
-
- expect(result).toEqual(featuredSongs);
- expect(songBrowserService.getFeaturedSongs).toHaveBeenCalled();
- });
- });
-
- describe('getSongList', () => {
- it('should return a list of recent songs', async () => {
- const query: PageQueryDTO = { page: 1, limit: 10 };
- const songList: SongPreviewDto[] = [];
-
- mockSongBrowserService.getRecentSongs.mockResolvedValueOnce(songList);
-
- const result = await controller.getSongList(query);
-
- expect(result).toEqual(songList);
- expect(songBrowserService.getRecentSongs).toHaveBeenCalledWith(query);
- });
- });
-
- describe('getCategories', () => {
- it('should return a list of song categories and song counts', async () => {
- const categories: Record = {
- category1: 10,
- category2: 5,
- };
-
- mockSongBrowserService.getCategories.mockResolvedValueOnce(categories);
-
- const result = await controller.getCategories();
-
- expect(result).toEqual(categories);
- expect(songBrowserService.getCategories).toHaveBeenCalled();
- });
- });
-
- describe('getSongsByCategory', () => {
- it('should return a list of songs by category', async () => {
- const id = 'test-category';
- const query: PageQueryDTO = { page: 1, limit: 10 };
- const songList: SongPreviewDto[] = [];
-
- mockSongBrowserService.getSongsByCategory.mockResolvedValueOnce(songList);
-
- const result = await controller.getSongsByCategory(id, query);
-
- expect(result).toEqual(songList);
-
- expect(songBrowserService.getSongsByCategory).toHaveBeenCalledWith(
- id,
- query,
- );
- });
- });
-});
diff --git a/apps/backend/src/song-browser/song-browser.controller.ts b/apps/backend/src/song-browser/song-browser.controller.ts
deleted file mode 100644
index f9746d1e..00000000
--- a/apps/backend/src/song-browser/song-browser.controller.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { FeaturedSongsDto, PageQueryDTO, SongPreviewDto } from '@nbw/database';
-import {
- BadRequestException,
- Controller,
- Get,
- Param,
- Query,
-} from '@nestjs/common';
-import { ApiOperation, ApiTags } from '@nestjs/swagger';
-
-import { SongBrowserService } from './song-browser.service';
-
-@Controller('song-browser')
-@ApiTags('song-browser')
-@ApiTags('song')
-export class SongBrowserController {
- constructor(public readonly songBrowserService: SongBrowserService) {}
-
- @Get('/featured')
- @ApiOperation({ summary: 'Get a list of featured songs' })
- public async getFeaturedSongs(): Promise {
- return await this.songBrowserService.getFeaturedSongs();
- }
-
- @Get('/recent')
- @ApiOperation({
- summary: 'Get a filtered/sorted list of recent songs with pagination',
- })
- public async getSongList(
- @Query() query: PageQueryDTO,
- ): Promise {
- return await this.songBrowserService.getRecentSongs(query);
- }
-
- @Get('/categories')
- @ApiOperation({ summary: 'Get a list of song categories and song counts' })
- public async getCategories(): Promise> {
- return await this.songBrowserService.getCategories();
- }
-
- @Get('/categories/:id')
- @ApiOperation({ summary: 'Get a list of song categories and song counts' })
- public async getSongsByCategory(
- @Param('id') id: string,
- @Query() query: PageQueryDTO,
- ): Promise {
- return await this.songBrowserService.getSongsByCategory(id, query);
- }
-
- @Get('/random')
- @ApiOperation({ summary: 'Get a list of songs at random' })
- public async getRandomSongs(
- @Query('count') count: string,
- @Query('category') category: string,
- ): Promise {
- const countInt = parseInt(count);
-
- if (isNaN(countInt) || countInt < 1 || countInt > 10) {
- throw new BadRequestException('Invalid query parameters');
- }
-
- return await this.songBrowserService.getRandomSongs(countInt, category);
- }
-}
diff --git a/apps/backend/src/song-browser/song-browser.module.ts b/apps/backend/src/song-browser/song-browser.module.ts
deleted file mode 100644
index f1eb25e0..00000000
--- a/apps/backend/src/song-browser/song-browser.module.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Module } from '@nestjs/common';
-
-import { SongModule } from '@server/song/song.module';
-
-import { SongBrowserController } from './song-browser.controller';
-import { SongBrowserService } from './song-browser.service';
-
-@Module({
- providers: [SongBrowserService],
- controllers: [SongBrowserController],
- imports: [SongModule],
-})
-export class SongBrowserModule {}
diff --git a/apps/backend/src/song-browser/song-browser.service.spec.ts b/apps/backend/src/song-browser/song-browser.service.spec.ts
deleted file mode 100644
index f46f98d6..00000000
--- a/apps/backend/src/song-browser/song-browser.service.spec.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import { PageQueryDTO, SongPreviewDto, SongWithUser } from '@nbw/database';
-import { HttpException } from '@nestjs/common';
-import { Test, TestingModule } from '@nestjs/testing';
-
-import { SongService } from '@server/song/song.service';
-
-import { SongBrowserService } from './song-browser.service';
-
-const mockSongService = {
- getSongsForTimespan: jest.fn(),
- getSongsBeforeTimespan: jest.fn(),
- getRecentSongs: jest.fn(),
- getCategories: jest.fn(),
- getSongsByCategory: jest.fn(),
-};
-
-describe('SongBrowserService', () => {
- let service: SongBrowserService;
- let songService: SongService;
-
- beforeEach(async () => {
- const module: TestingModule = await Test.createTestingModule({
- providers: [
- SongBrowserService,
- {
- provide: SongService,
- useValue: mockSongService,
- },
- ],
- }).compile();
-
- service = module.get(SongBrowserService);
- songService = module.get(SongService);
- });
-
- it('should be defined', () => {
- expect(service).toBeDefined();
- });
-
- describe('getFeaturedSongs', () => {
- it('should return featured songs', async () => {
- const songWithUser: SongWithUser = {
- title: 'Test Song',
- uploader: { username: 'testuser', profileImage: 'testimage' },
- stats: {
- duration: 100,
- noteCount: 100,
- },
- } as any;
-
- jest
- .spyOn(songService, 'getSongsForTimespan')
- .mockResolvedValue([songWithUser]);
-
- jest
- .spyOn(songService, 'getSongsBeforeTimespan')
- .mockResolvedValue([songWithUser]);
-
- await service.getFeaturedSongs();
-
- expect(songService.getSongsForTimespan).toHaveBeenCalled();
- expect(songService.getSongsBeforeTimespan).toHaveBeenCalled();
- });
- });
-
- describe('getRecentSongs', () => {
- it('should return recent songs', async () => {
- const query: PageQueryDTO = { page: 1, limit: 10 };
-
- const songPreviewDto: SongPreviewDto = {
- title: 'Test Song',
- uploader: { username: 'testuser', profileImage: 'testimage' },
- } as any;
-
- jest
- .spyOn(songService, 'getRecentSongs')
- .mockResolvedValue([songPreviewDto]);
-
- const result = await service.getRecentSongs(query);
-
- expect(result).toEqual([songPreviewDto]);
-
- expect(songService.getRecentSongs).toHaveBeenCalledWith(
- query.page,
- query.limit,
- );
- });
-
- it('should throw an error if query parameters are invalid', async () => {
- const query: PageQueryDTO = { page: undefined, limit: undefined };
-
- await expect(service.getRecentSongs(query)).rejects.toThrow(
- HttpException,
- );
- });
- });
-
- describe('getCategories', () => {
- it('should return categories', async () => {
- const categories = { pop: 10, rock: 5 };
-
- jest.spyOn(songService, 'getCategories').mockResolvedValue(categories);
-
- const result = await service.getCategories();
-
- expect(result).toEqual(categories);
- expect(songService.getCategories).toHaveBeenCalled();
- });
- });
-
- describe('getSongsByCategory', () => {
- it('should return songs by category', async () => {
- const category = 'pop';
- const query: PageQueryDTO = { page: 1, limit: 10 };
-
- const songPreviewDto: SongPreviewDto = {
- title: 'Test Song',
- uploader: { username: 'testuser', profileImage: 'testimage' },
- } as any;
-
- jest
- .spyOn(songService, 'getSongsByCategory')
- .mockResolvedValue([songPreviewDto]);
-
- const result = await service.getSongsByCategory(category, query);
-
- expect(result).toEqual([songPreviewDto]);
-
- expect(songService.getSongsByCategory).toHaveBeenCalledWith(
- category,
- query.page,
- query.limit,
- );
- });
- });
-});
diff --git a/apps/backend/src/song-browser/song-browser.service.ts b/apps/backend/src/song-browser/song-browser.service.ts
deleted file mode 100644
index 0739d5c0..00000000
--- a/apps/backend/src/song-browser/song-browser.service.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { BROWSER_SONGS } from '@nbw/config';
-import {
- FeaturedSongsDto,
- PageQueryDTO,
- SongPreviewDto,
- SongWithUser,
- TimespanType,
-} from '@nbw/database';
-import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
-
-import { SongService } from '@server/song/song.service';
-
-@Injectable()
-export class SongBrowserService {
- constructor(
- @Inject(SongService)
- private songService: SongService,
- ) {}
-
- public async getFeaturedSongs(): Promise {
- const now = new Date(Date.now());
-
- const times: Record = {
- hour: new Date(Date.now()).setHours(now.getHours() - 1),
- day: new Date(Date.now()).setDate(now.getDate() - 1),
- week: new Date(Date.now()).setDate(now.getDate() - 7),
- month: new Date(Date.now()).setMonth(now.getMonth() - 1),
- year: new Date(Date.now()).setFullYear(now.getFullYear() - 1),
- all: new Date(0).getTime(),
- };
-
- const songs: Record = {
- hour: [],
- day: [],
- week: [],
- month: [],
- year: [],
- all: [],
- };
-
- for (const [timespan, time] of Object.entries(times)) {
- const songPage = await this.songService.getSongsForTimespan(time);
-
- // If the length is 0, send an empty array (no songs available in that timespan)
- // If the length is less than the page size, pad it with songs "borrowed"
- // from the nearest timestamp, regardless of view count
- if (
- songPage.length > 0 &&
- songPage.length < BROWSER_SONGS.paddedFeaturedPageSize
- ) {
- const missing = BROWSER_SONGS.paddedFeaturedPageSize - songPage.length;
-
- const additionalSongs = await this.songService.getSongsBeforeTimespan(
- time,
- );
-
- songPage.push(...additionalSongs.slice(0, missing));
- }
-
- songs[timespan as TimespanType] = songPage;
- }
-
- const featuredSongs = FeaturedSongsDto.create();
-
- featuredSongs.hour = songs.hour.map((song) =>
- SongPreviewDto.fromSongDocumentWithUser(song),
- );
-
- featuredSongs.day = songs.day.map((song) =>
- SongPreviewDto.fromSongDocumentWithUser(song),
- );
-
- featuredSongs.week = songs.week.map((song) =>
- SongPreviewDto.fromSongDocumentWithUser(song),
- );
-
- featuredSongs.month = songs.month.map((song) =>
- SongPreviewDto.fromSongDocumentWithUser(song),
- );
-
- featuredSongs.year = songs.year.map((song) =>
- SongPreviewDto.fromSongDocumentWithUser(song),
- );
-
- featuredSongs.all = songs.all.map((song) =>
- SongPreviewDto.fromSongDocumentWithUser(song),
- );
-
- return featuredSongs;
- }
-
- public async getRecentSongs(query: PageQueryDTO): Promise {
- const { page, limit } = query;
-
- if (!page || !limit) {
- throw new HttpException(
- 'Invalid query parameters',
- HttpStatus.BAD_REQUEST,
- );
- }
-
- return await this.songService.getRecentSongs(page, limit);
- }
-
- public async getCategories(): Promise> {
- return await this.songService.getCategories();
- }
-
- public async getSongsByCategory(
- category: string,
- query: PageQueryDTO,
- ): Promise {
- return await this.songService.getSongsByCategory(
- category,
- query.page ?? 1,
- query.limit ?? 10,
- );
- }
-
- public async getRandomSongs(
- count: number,
- category: string,
- ): Promise {
- return await this.songService.getRandomSongs(count, category);
- }
-}
diff --git a/apps/backend/src/song/my-songs/my-songs.controller.spec.ts b/apps/backend/src/song/my-songs/my-songs.controller.spec.ts
index 25b3d33f..541b27fb 100644
--- a/apps/backend/src/song/my-songs/my-songs.controller.spec.ts
+++ b/apps/backend/src/song/my-songs/my-songs.controller.spec.ts
@@ -5,6 +5,7 @@ import { AuthGuard } from '@nestjs/passport';
import { Test, TestingModule } from '@nestjs/testing';
import { SongService } from '../song.service';
+
import { MySongsController } from './my-songs.controller';
const mockSongService = {
@@ -18,9 +19,9 @@ describe('MySongsController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MySongsController],
- providers: [
+ providers : [
{
- provide: SongService,
+ provide : SongService,
useValue: mockSongService,
},
],
@@ -44,9 +45,9 @@ describe('MySongsController', () => {
const songPageDto: SongPageDto = {
content: [],
- page: 0,
- limit: 0,
- total: 0,
+ page : 0,
+ limit : 0,
+ total : 0,
};
mockSongService.getMySongsPage.mockResolvedValueOnce(songPageDto);
diff --git a/apps/backend/src/song/song-upload/song-upload.service.spec.ts b/apps/backend/src/song/song-upload/song-upload.service.spec.ts
index c58c0d22..a78388a4 100644
--- a/apps/backend/src/song/song-upload/song-upload.service.spec.ts
+++ b/apps/backend/src/song/song-upload/song-upload.service.spec.ts
@@ -1,3 +1,5 @@
+import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test';
+
import { Instrument, Layer, Note, Song } from '@encode42/nbs.js';
import type { UserDocument } from '@nbw/database';
import {
@@ -8,7 +10,6 @@ import {
} from '@nbw/database';
import { HttpException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
-import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test';
import { Types } from 'mongoose';
import { FileService } from '@server/file/file.service';
@@ -22,10 +23,10 @@ mock.module('@nbw/thumbnail', () => ({
}));
const mockFileService = {
- uploadSong: jest.fn(),
+ uploadSong : jest.fn(),
uploadPackedSong: jest.fn(),
- uploadThumbnail: jest.fn(),
- getSongFile: jest.fn(),
+ uploadThumbnail : jest.fn(),
+ getSongFile : jest.fn(),
};
const mockUserService = {
@@ -42,11 +43,11 @@ describe('SongUploadService', () => {
providers: [
SongUploadService,
{
- provide: FileService,
+ provide : FileService,
useValue: mockFileService,
},
{
- provide: UserService,
+ provide : UserService,
useValue: mockUserService,
},
],
@@ -66,26 +67,26 @@ describe('SongUploadService', () => {
const file = { buffer: Buffer.from('test') } as Express.Multer.File;
const user: UserDocument = {
- _id: new Types.ObjectId(),
+ _id : new Types.ObjectId(),
username: 'testuser',
} as UserDocument;
const body: UploadSongDto = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
allowDownload: true,
- file: 'somebytes',
+ file : 'somebytes',
};
const songEntity = new SongEntity();
@@ -96,7 +97,7 @@ describe('SongUploadService', () => {
);
spyOn(songUploadService as any, 'prepareSongForUpload').mockReturnValue({
- nbsSong: new Song(),
+ nbsSong : new Song(),
songBuffer: Buffer.from('test'),
});
@@ -161,42 +162,42 @@ describe('SongUploadService', () => {
describe('processSongPatch', () => {
it('should process and patch a song', async () => {
const user: UserDocument = {
- _id: new Types.ObjectId(),
+ _id : new Types.ObjectId(),
username: 'testuser',
} as UserDocument;
const body: UploadSongDto = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
allowDownload: true,
- file: 'somebytes',
+ file : 'somebytes',
};
const songDocument: SongDocument = {
...body,
- publicId: 'test-id',
- uploader: user._id,
+ publicId : 'test-id',
+ uploader : user._id,
customInstruments: [],
- thumbnailData: body.thumbnailData,
- nbsFileUrl: 'http://test.com/file.nbs',
- save: jest.fn().mockResolvedValue({}),
+ thumbnailData : body.thumbnailData,
+ nbsFileUrl : 'http://test.com/file.nbs',
+ save : jest.fn().mockResolvedValue({}),
} as any;
spyOn(fileService, 'getSongFile').mockResolvedValue(new ArrayBuffer(0));
spyOn(songUploadService as any, 'prepareSongForUpload').mockReturnValue({
- nbsSong: new Song(),
+ nbsSong : new Song(),
songBuffer: Buffer.from('test'),
});
@@ -224,9 +225,9 @@ describe('SongUploadService', () => {
describe('generateAndUploadThumbnail', () => {
it('should generate and upload a thumbnail', async () => {
const thumbnailData: ThumbnailData = {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
};
@@ -255,9 +256,9 @@ describe('SongUploadService', () => {
it('should throw an error if the thumbnail is invalid', async () => {
const thumbnailData: ThumbnailData = {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
};
@@ -353,10 +354,10 @@ describe('SongUploadService', () => {
const songTest = new Song();
songTest.meta = {
- author: 'Nicolas Vycas',
- description: 'super cool song',
- importName: 'test',
- name: 'Cool Test Song',
+ author : 'Nicolas Vycas',
+ description : 'super cool song',
+ importName : 'test',
+ name : 'Cool Test Song',
originalAuthor: 'Nicolas Vycas',
};
diff --git a/apps/backend/src/song/song-upload/song-upload.service.ts b/apps/backend/src/song/song-upload/song-upload.service.ts
index bbf37b9d..6be00500 100644
--- a/apps/backend/src/song/song-upload/song-upload.service.ts
+++ b/apps/backend/src/song/song-upload/song-upload.service.ts
@@ -1,26 +1,8 @@
import { Song, fromArrayBuffer, toArrayBuffer } from '@encode42/nbs.js';
-import {
- SongDocument,
- Song as SongEntity,
- SongStats,
- ThumbnailData,
- UploadSongDto,
- UserDocument,
-} from '@nbw/database';
-import {
- NoteQuadTree,
- SongStatsGenerator,
- injectSongFileMetadata,
- obfuscateAndPackSong,
-} from '@nbw/song';
+import { SongDocument, Song as SongEntity, SongStats, ThumbnailData, UploadSongDto, UserDocument, } from '@nbw/database';
+import { NoteQuadTree, SongStatsGenerator, injectSongFileMetadata, obfuscateAndPackSong, } from '@nbw/song';
import { drawToImage } from '@nbw/thumbnail';
-import {
- HttpException,
- HttpStatus,
- Inject,
- Injectable,
- Logger,
-} from '@nestjs/common';
+import { HttpException, HttpStatus, Inject, Injectable, Logger, } from '@nestjs/common';
import { Types } from 'mongoose';
import { FileService } from '@server/file/file.service';
@@ -115,7 +97,7 @@ export class SongUploadService {
song.originalAuthor = removeExtraSpaces(body.originalAuthor);
song.description = removeExtraSpaces(body.description);
song.category = body.category;
- song.allowDownload = true || body.allowDownload; //TODO: implement allowDownload;
+ song.allowDownload = true ;//|| body.allowDownload; //TODO: implement allowDownload;
song.visibility = body.visibility;
song.license = body.license;
@@ -360,13 +342,13 @@ export class SongUploadService {
const quadTree = new NoteQuadTree(nbsSong);
const thumbBuffer = await drawToImage({
- notes: quadTree,
- startTick: startTick,
- startLayer: startLayer,
- zoomLevel: zoomLevel,
+ notes : quadTree,
+ startTick : startTick,
+ startLayer : startLayer,
+ zoomLevel : zoomLevel,
backgroundColor: backgroundColor,
- imgWidth: 1280,
- imgHeight: 768,
+ imgWidth : 1280,
+ imgHeight : 768,
});
// Upload thumbnail
@@ -446,7 +428,7 @@ export class SongUploadService {
throw new HttpException(
{
error: {
- file: 'Invalid NBS file',
+ file : 'Invalid NBS file',
errors: nbsSong.errors,
},
},
diff --git a/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts b/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts
index 8b09fef1..158b0912 100644
--- a/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts
+++ b/apps/backend/src/song/song-webhook/song-webhook.service.spec.ts
@@ -1,11 +1,13 @@
+import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test';
+
import { Song as SongEntity, SongWithUser } from '@nbw/database';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
-import { beforeEach, describe, expect, it, jest, mock, spyOn } from 'bun:test';
import { Model } from 'mongoose';
import { getUploadDiscordEmbed } from '../song.util';
+
import { SongWebhookService } from './song-webhook.service';
mock.module('../song.util', () => ({
@@ -13,10 +15,10 @@ mock.module('../song.util', () => ({
}));
const mockSongModel = {
- find: jest.fn().mockReturnThis(),
- sort: jest.fn().mockReturnThis(),
+ find : jest.fn().mockReturnThis(),
+ sort : jest.fn().mockReturnThis(),
populate: jest.fn().mockReturnThis(),
- save: jest.fn(),
+ save : jest.fn(),
};
describe('SongWebhookService', () => {
@@ -26,15 +28,15 @@ describe('SongWebhookService', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
- imports: [ConfigModule.forRoot()],
+ imports : [ConfigModule.forRoot()],
providers: [
SongWebhookService,
{
- provide: getModelToken(SongEntity.name),
+ provide : getModelToken(SongEntity.name),
useValue: mockSongModel,
},
{
- provide: 'DISCORD_WEBHOOK_URL',
+ provide : 'DISCORD_WEBHOOK_URL',
useValue: 'http://localhost/webhook',
},
],
@@ -67,7 +69,7 @@ describe('SongWebhookService', () => {
expect(result).toBe('message-id');
expect(fetch).toHaveBeenCalledWith('http://localhost/webhook?wait=true', {
- method: 'POST',
+ method : 'POST',
headers: {
'Content-Type': 'application/json',
},
@@ -94,9 +96,9 @@ describe('SongWebhookService', () => {
describe('updateSongWebhook', () => {
it('should update the webhook message for a song', async () => {
const song: SongWithUser = {
- publicId: '123',
+ publicId : '123',
webhookMessageId: 'message-id',
- uploader: { username: 'testuser', profileImage: 'testimage' },
+ uploader : { username: 'testuser', profileImage: 'testimage' },
} as SongWithUser;
(getUploadDiscordEmbed as jest.Mock).mockReturnValue({});
@@ -108,7 +110,7 @@ describe('SongWebhookService', () => {
expect(fetch).toHaveBeenCalledWith(
'http://localhost/webhook/messages/message-id',
{
- method: 'PATCH',
+ method : 'PATCH',
headers: {
'Content-Type': 'application/json',
},
@@ -119,9 +121,9 @@ describe('SongWebhookService', () => {
it('should log an error if there is an error', async () => {
const song: SongWithUser = {
- publicId: '123',
+ publicId : '123',
webhookMessageId: 'message-id',
- uploader: { username: 'testuser', profileImage: 'testimage' },
+ uploader : { username: 'testuser', profileImage: 'testimage' },
} as SongWithUser;
(getUploadDiscordEmbed as jest.Mock).mockReturnValue({});
@@ -142,9 +144,9 @@ describe('SongWebhookService', () => {
describe('deleteSongWebhook', () => {
it('should delete the webhook message for a song', async () => {
const song: SongWithUser = {
- publicId: '123',
+ publicId : '123',
webhookMessageId: 'message-id',
- uploader: { username: 'testuser', profileImage: 'testimage' },
+ uploader : { username: 'testuser', profileImage: 'testimage' },
} as SongWithUser;
(global as any).fetch = jest.fn().mockResolvedValue({});
@@ -161,9 +163,9 @@ describe('SongWebhookService', () => {
it('should log an error if there is an error', async () => {
const song: SongWithUser = {
- publicId: '123',
+ publicId : '123',
webhookMessageId: 'message-id',
- uploader: { username: 'testuser', profileImage: 'testimage' },
+ uploader : { username: 'testuser', profileImage: 'testimage' },
} as SongWithUser;
(global as any).fetch = jest.fn().mockRejectedValue(new Error('Error'));
@@ -182,10 +184,10 @@ describe('SongWebhookService', () => {
describe('syncSongWebhook', () => {
it('should update the webhook message if the song is public', async () => {
const song: SongWithUser = {
- publicId: '123',
+ publicId : '123',
webhookMessageId: 'message-id',
- visibility: 'public',
- uploader: { username: 'testuser', profileImage: 'testimage' },
+ visibility : 'public',
+ uploader : { username: 'testuser', profileImage: 'testimage' },
} as SongWithUser;
const updateSpy = spyOn(service, 'updateSongWebhook');
@@ -197,10 +199,10 @@ describe('SongWebhookService', () => {
it('should delete the webhook message if the song is not public', async () => {
const song: SongWithUser = {
- publicId: '123',
+ publicId : '123',
webhookMessageId: 'message-id',
- visibility: 'private',
- uploader: { username: 'testuser', profileImage: 'testimage' },
+ visibility : 'private',
+ uploader : { username: 'testuser', profileImage: 'testimage' },
} as SongWithUser;
const deleteSpy = spyOn(service, 'deleteSongWebhook');
@@ -212,9 +214,9 @@ describe('SongWebhookService', () => {
it('should post a new webhook message if the song is public and does not have a message', async () => {
const song: SongWithUser = {
- publicId: '123',
+ publicId : '123',
visibility: 'public',
- uploader: { username: 'testuser', profileImage: 'testimage' },
+ uploader : { username: 'testuser', profileImage: 'testimage' },
} as SongWithUser;
const postSpy = spyOn(service, 'postSongWebhook');
@@ -226,9 +228,9 @@ describe('SongWebhookService', () => {
it('should return null if the song is not public and does not have a message', async () => {
const song: SongWithUser = {
- publicId: '123',
+ publicId : '123',
visibility: 'private',
- uploader: { username: 'testuser', profileImage: 'testimage' },
+ uploader : { username: 'testuser', profileImage: 'testimage' },
} as SongWithUser;
const result = await service.syncSongWebhook(song);
@@ -243,12 +245,12 @@ describe('SongWebhookService', () => {
{
publicId: '123',
uploader: { username: 'testuser', profileImage: 'testimage' },
- save: jest.fn(),
+ save : jest.fn(),
} as unknown as SongWithUser,
];
mockSongModel.find.mockReturnValue({
- sort: jest.fn().mockReturnThis(),
+ sort : jest.fn().mockReturnThis(),
populate: jest.fn().mockResolvedValue(songs),
});
diff --git a/apps/backend/src/song/song-webhook/song-webhook.service.ts b/apps/backend/src/song/song-webhook/song-webhook.service.ts
index 90e16745..18681e94 100644
--- a/apps/backend/src/song/song-webhook/song-webhook.service.ts
+++ b/apps/backend/src/song/song-webhook/song-webhook.service.ts
@@ -41,7 +41,7 @@ export class SongWebhookService implements OnModuleInit {
try {
const response = await fetch(`${webhookUrl}?wait=true`, {
- method: 'POST',
+ method : 'POST',
headers: {
'Content-Type': 'application/json',
},
@@ -84,7 +84,7 @@ export class SongWebhookService implements OnModuleInit {
try {
await fetch(`${webhookUrl}/messages/${song.webhookMessageId}`, {
- method: 'PATCH',
+ method : 'PATCH',
headers: {
'Content-Type': 'application/json',
},
diff --git a/apps/backend/src/song/song.controller.spec.ts b/apps/backend/src/song/song.controller.spec.ts
index f4fc3ac2..5f151a76 100644
--- a/apps/backend/src/song/song.controller.spec.ts
+++ b/apps/backend/src/song/song.controller.spec.ts
@@ -1,29 +1,24 @@
import type { UserDocument } from '@nbw/database';
-import {
- PageQueryDTO,
- SongPreviewDto,
- SongViewDto,
- UploadSongDto,
- UploadSongResponseDto,
-} from '@nbw/database';
+import { PageQueryDTO, SongPreviewDto, SongViewDto, UploadSongDto, UploadSongResponseDto, } from '@nbw/database';
import { HttpStatus, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Test, TestingModule } from '@nestjs/testing';
import { Response } from 'express';
-import { FileService } from '@server/file/file.service';
+import { FileService } from '../file/file.service';
import { SongController } from './song.controller';
import { SongService } from './song.service';
const mockSongService = {
- getSongByPage: jest.fn(),
- getSong: jest.fn(),
- getSongEdit: jest.fn(),
- patchSong: jest.fn(),
+ getSongByPage : jest.fn(),
+ searchSongs : jest.fn(),
+ getSong : jest.fn(),
+ getSongEdit : jest.fn(),
+ patchSong : jest.fn(),
getSongDownloadUrl: jest.fn(),
- deleteSong: jest.fn(),
- uploadSong: jest.fn(),
+ deleteSong : jest.fn(),
+ uploadSong : jest.fn(),
};
const mockFileService = {};
@@ -35,13 +30,13 @@ describe('SongController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SongController],
- providers: [
+ providers : [
{
- provide: SongService,
+ provide : SongService,
useValue: mockSongService,
},
{
- provide: FileService,
+ provide : FileService,
useValue: mockFileService,
},
],
@@ -52,6 +47,9 @@ describe('SongController', () => {
songController = module.get(SongController);
songService = module.get(SongService);
+
+ // Clear all mocks
+ jest.clearAllMocks();
});
it('should be defined', () => {
@@ -71,6 +69,83 @@ describe('SongController', () => {
expect(songService.getSongByPage).toHaveBeenCalledWith(query);
});
+ it('should handle featured songs', async () => {
+ const query: PageQueryDTO = { page: 1, limit: 10 };
+ const songList: SongPreviewDto[] = [];
+
+ const result = await songController.getSongList(query, 'featured');
+
+ expect(result).toEqual(songList);
+ });
+
+ it('should handle recent songs', async () => {
+ const query: PageQueryDTO = { page: 1, limit: 10 };
+ const songList: SongPreviewDto[] = [];
+
+
+ const result = await songController.getSongList(query, 'recent');
+
+ expect(result).toEqual(songList);
+ });
+
+ it('should return categories when q=categories without id', async () => {
+ const query: PageQueryDTO = { page: 1, limit: 10 };
+ const categories = { pop: 42, rock: 38 };
+
+
+ const result = await songController.getSongList(query, 'categories');
+
+ expect(result).toEqual(categories);
+ });
+
+ it('should return songs by category when q=categories with id', async () => {
+ const query: PageQueryDTO = { page: 1, limit: 10 };
+ const songList: SongPreviewDto[] = [];
+ const categoryId = 'pop';
+
+
+ const result = await songController.getSongList(query, 'categories', categoryId);
+
+ expect(result).toEqual(songList);
+ });
+
+ it('should return random songs', async () => {
+ const query: PageQueryDTO = { page: 1, limit: 5 };
+ const songList: SongPreviewDto[] = [];
+ const category = 'electronic';
+
+
+ const result = await songController.getSongList(query, 'random', undefined, category);
+
+ expect(result).toEqual(songList);
+ });
+
+ it('should throw error for invalid random count', async () => {
+ const query: PageQueryDTO = { page: 1, limit: 15 }; // Invalid limit > 10
+
+ await expect(
+ songController.getSongList(query, 'random')
+ ).rejects.toThrow('Invalid query parameters');
+ });
+
+ it('should handle zero limit for random (uses default)', async () => {
+ const query: PageQueryDTO = { page: 1, limit: 0 }; // limit 0 is falsy, so uses default
+ const songList: SongPreviewDto[] = [];
+
+
+ const result = await songController.getSongList(query, 'random');
+
+ expect(result).toEqual(songList);
+ });
+
+ it('should throw error for invalid query mode', async () => {
+ const query: PageQueryDTO = { page: 1, limit: 10 };
+
+ await expect(
+ songController.getSongList(query, 'invalid' as any)
+ ).rejects.toThrow('Invalid query parameters');
+ });
+
it('should handle errors', async () => {
const query: PageQueryDTO = { page: 1, limit: 10 };
@@ -165,7 +240,7 @@ describe('SongController', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const res = {
- set: jest.fn(),
+ set : jest.fn(),
redirect: jest.fn(),
} as unknown as Response;
@@ -176,7 +251,7 @@ describe('SongController', () => {
await songController.getSongFile(id, src, user, res);
expect(res.set).toHaveBeenCalledWith({
- 'Content-Disposition': 'attachment; filename="song.nbs"',
+ 'Content-Disposition' : 'attachment; filename="song.nbs"',
'Access-Control-Expose-Headers': 'Content-Disposition',
});
@@ -196,7 +271,7 @@ describe('SongController', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const res = {
- set: jest.fn(),
+ set : jest.fn(),
redirect: jest.fn(),
} as unknown as Response;
@@ -285,20 +360,20 @@ describe('SongController', () => {
const file = { buffer: Buffer.from('test') } as Express.Multer.File;
const body: UploadSongDto = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'cc_by_sa',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'cc_by_sa',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
- file: undefined,
+ file : undefined,
allowDownload: false,
};
@@ -317,20 +392,20 @@ describe('SongController', () => {
const file = { buffer: Buffer.from('test') } as Express.Multer.File;
const body: UploadSongDto = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'cc_by_sa',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'cc_by_sa',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
- file: undefined,
+ file : undefined,
allowDownload: false,
};
diff --git a/apps/backend/src/song/song.controller.ts b/apps/backend/src/song/song.controller.ts
index d6711cee..67118d9f 100644
--- a/apps/backend/src/song/song.controller.ts
+++ b/apps/backend/src/song/song.controller.ts
@@ -1,41 +1,11 @@
import { UPLOAD_CONSTANTS } from '@nbw/config';
-import type { UserDocument } from '@nbw/database';
-import {
- PageQueryDTO,
- SongPreviewDto,
- SongViewDto,
- UploadSongDto,
- UploadSongResponseDto,
-} from '@nbw/database';
+import { PageDto, UserDocument, PageQueryDTO, SongPreviewDto, SongViewDto, UploadSongDto, UploadSongResponseDto, FeaturedSongsDto, } from '@nbw/database';
import type { RawBodyRequest } from '@nestjs/common';
-import {
- Body,
- Controller,
- Delete,
- Get,
- Headers,
- HttpStatus,
- Param,
- Patch,
- Post,
- Query,
- Req,
- Res,
- UnauthorizedException,
- UploadedFile,
- UseGuards,
- UseInterceptors,
-} from '@nestjs/common';
+import { BadRequestException, Body, Controller, Delete, Get, Headers, HttpStatus, Param, Patch, Post, Query, Req, Res, UnauthorizedException, UploadedFile, UseGuards, UseInterceptors, } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from '@nestjs/platform-express';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
-import {
- ApiBearerAuth,
- ApiBody,
- ApiConsumes,
- ApiOperation,
- ApiTags,
-} from '@nestjs/swagger';
+import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags, } from '@nestjs/swagger';
import type { Response } from 'express';
import { FileService } from '@server/file/file.service';
@@ -43,45 +13,150 @@ import { GetRequestToken, validateUser } from '@server/lib/GetRequestUser';
import { SongService } from './song.service';
-// Handles public-facing song routes.
-
@Controller('song')
@ApiTags('song')
export class SongController {
static multerConfig: MulterOptions = {
- limits: {
- fileSize: UPLOAD_CONSTANTS.file.maxSize,
- },
+ limits : { fileSize: UPLOAD_CONSTANTS.file.maxSize },
fileFilter: (req, file, cb) => {
- if (!file.originalname.match(/\.(nbs)$/)) {
+ if (!file.originalname.match(/\.(nbs)$/))
return cb(new Error('Only .nbs files are allowed!'), false);
- }
-
cb(null, true);
},
};
- constructor(
- public readonly songService: SongService,
- public readonly fileService: FileService,
- ) {}
+ constructor(public readonly songService: SongService, public readonly fileService: FileService,) {}
@Get('/')
@ApiOperation({
- summary: 'Get a filtered/sorted list of songs with pagination',
+ summary : 'Get songs with various filtering and browsing options',
+ description: `
+ Retrieves songs based on the provided query parameters. Supports multiple modes:
+
+ **Default mode** (no 'q' parameter): Returns paginated songs with sorting/filtering
+
+ **Special query modes** (using 'q' parameter):
+ - \`featured\`: Get recent popular songs with pagination
+ - \`recent\`: Get recently uploaded songs with pagination
+ - \`categories\`:
+ - Without 'id': Returns a record of available categories and their song counts
+ - With 'id': Returns songs from the specified category with pagination
+ - \`random\`: Returns random songs (requires 'count' parameter, 1-10 songs, optionally filtered by 'category')
+
+ **Query Parameters:**
+ - Standard pagination/sorting via PageQueryDTO (page, limit, sort, order, timespan)
+ - \`q\`: Special query mode ('featured', 'recent', 'categories', 'random')
+ - \`id\`: Category ID (used with q=categories to get songs from specific category)
+ - \`count\`: Number of random songs to return (1-10, used with q=random)
+ - \`category\`: Category filter for random songs (used with q=random)
+
+ **Return Types:**
+ - SongPreviewDto[]: Array of song previews (most cases)
+ - Record: Category name to count mapping (when q=categories without id)
+ `,
+ })
+ @ApiQuery({ name: 'q', required: false, enum: ['featured', 'recent', 'categories', 'random'], description: 'Special query mode. If not provided, returns standard paginated song list.', example: 'recent', })
+ @ApiParam({ name: 'id', required: false, type: 'string', description: 'Category ID. Only used when q=categories to get songs from a specific category.', example: 'pop', })
+ @ApiQuery({ name: 'count', required: false, type: 'string', description: 'Number of random songs to return (1-10). Only used when q=random.', example: '5', })
+ @ApiQuery({ name: 'category', required: false, type: 'string', description: 'Category filter for random songs. Only used when q=random.', example: 'electronic', })
+ @ApiResponse({
+ status : 200,
+ description: 'Success. Returns either an array of song previews or category counts.',
+ schema : {
+ oneOf: [
+ {
+ type : 'array',
+ items : { $ref: '#/components/schemas/SongPreviewDto' },
+ description: 'Array of song previews (default behavior and most query modes)',
+ },
+ {
+ type : 'object',
+ additionalProperties: { type: 'number' },
+ description : 'Category name to song count mapping (only when q=categories without id)',
+ example : { pop: 42, rock: 38, electronic: 15 },
+ },
+ ],
+ },
+ })
+ @ApiResponse({
+ status : 400,
+ description: 'Bad Request. Invalid query parameters (e.g., invalid count for random query).',
})
public async getSongList(
@Query() query: PageQueryDTO,
- ): Promise {
- return await this.songService.getSongByPage(query);
+ @Query('q') q?: 'featured' | 'recent' | 'categories' | 'random',
+ @Param('id') id?: string,
+ @Query('category') category?: string,
+ ): Promise | Record | FeaturedSongsDto> {
+ if (q) {
+ switch (q) {
+ case 'featured':
+ return await this.songService.getFeaturedSongs();
+ case 'recent':
+ return new PageDto({
+ content: await this.songService.getRecentSongs(query.page, query.limit,),
+ page : query.page,
+ limit : query.limit,
+ total : 0,
+ });
+ case 'categories':
+ if (id) {
+ return new PageDto({
+ content: await this.songService.getSongsByCategory(
+ category,
+ query.page,
+ query.limit,
+ ),
+ page : query.page,
+ limit: query.limit,
+ total: 0,
+ });
+ }
+ return await this.songService.getCategories();
+ case 'random': {
+ if (query.limit && (query.limit < 1 || query.limit > 10)) {
+ throw new BadRequestException('Invalid query parameters');
+ }
+ const data = await this.songService.getRandomSongs(
+ query.limit ?? 1,
+ category,
+ );
+ return new PageDto({
+ content: data,
+ page : query.page,
+ limit : query.limit,
+ total : data.length,
+ });
+ }
+ default:
+ throw new BadRequestException('Invalid query parameters');
+ }
+ }
+
+ const data = await this.songService.getSongByPage(query);
+ return new PageDto({
+ content: data,
+ page : query.page,
+ limit : query.limit,
+ total : data.length,
+ });
+ }
+
+ @Get('/search')
+ @ApiOperation({ summary: 'Search songs by keywords with pagination and sorting', })
+ public async searchSongs(@Query() query: PageQueryDTO, @Query('q') q: string,): Promise> {
+ const data = await this.songService.searchSongs(query, q ?? '');
+ return new PageDto({
+ content: data,
+ page : query.page,
+ limit : query.limit,
+ total : data.length,
+ });
}
@Get('/:id')
@ApiOperation({ summary: 'Get song info by ID' })
- public async getSong(
- @Param('id') id: string,
- @GetRequestToken() user: UserDocument | null,
- ): Promise {
+ public async getSong(@Param('id') id: string, @GetRequestToken() user: UserDocument | null,): Promise {
return await this.songService.getSong(id, user);
}
@@ -89,10 +164,7 @@ export class SongController {
@ApiOperation({ summary: 'Get song info for editing by ID' })
@UseGuards(AuthGuard('jwt-refresh'))
@ApiBearerAuth()
- public async getEditSong(
- @Param('id') id: string,
- @GetRequestToken() user: UserDocument | null,
- ): Promise {
+ public async getEditSong(@Param('id') id: string, @GetRequestToken() user: UserDocument | null,): Promise {
user = validateUser(user);
return await this.songService.getSongEdit(id, user);
}
@@ -101,15 +173,8 @@ export class SongController {
@UseGuards(AuthGuard('jwt-refresh'))
@ApiBearerAuth()
@ApiOperation({ summary: 'Edit song info by ID' })
- @ApiBody({
- description: 'Upload Song',
- type: UploadSongResponseDto,
- })
- public async patchSong(
- @Param('id') id: string,
- @Req() req: RawBodyRequest,
- @GetRequestToken() user: UserDocument | null,
- ): Promise {
+ @ApiBody({ description: 'Upload Song', type: UploadSongResponseDto, })
+ public async patchSong(@Param('id') id: string, @Req() req: RawBodyRequest, @GetRequestToken() user: UserDocument | null,): Promise {
user = validateUser(user);
//TODO: Fix this weird type casting and raw body access
const body = req.body as unknown as UploadSongDto;
@@ -118,17 +183,12 @@ export class SongController {
@Get('/:id/download')
@ApiOperation({ summary: 'Get song .nbs file' })
- public async getSongFile(
- @Param('id') id: string,
- @Query('src') src: string,
- @GetRequestToken() user: UserDocument | null,
- @Res() res: Response,
- ): Promise {
+ public async getSongFile(@Param('id') id: string, @Query('src') src: string, @GetRequestToken() user: UserDocument | null, @Res() res: Response,): Promise {
user = validateUser(user);
// TODO: no longer used
res.set({
- 'Content-Disposition': 'attachment; filename="song.nbs"',
+ 'Content-Disposition' : 'attachment; filename="song.nbs"',
// Expose the Content-Disposition header to the client
'Access-Control-Expose-Headers': 'Content-Disposition',
});
@@ -139,11 +199,7 @@ export class SongController {
@Get('/:id/open')
@ApiOperation({ summary: 'Get song .nbs file' })
- public async getSongOpenUrl(
- @Param('id') id: string,
- @GetRequestToken() user: UserDocument | null,
- @Headers('src') src: string,
- ): Promise {
+ public async getSongOpenUrl(@Param('id') id: string, @GetRequestToken() user: UserDocument | null, @Headers('src') src: string,): Promise {
if (src != 'downloadButton') {
throw new UnauthorizedException('Invalid source');
}
@@ -162,10 +218,7 @@ export class SongController {
@UseGuards(AuthGuard('jwt-refresh'))
@ApiBearerAuth()
@ApiOperation({ summary: 'Delete a song' })
- public async deleteSong(
- @Param('id') id: string,
- @GetRequestToken() user: UserDocument | null,
- ): Promise {
+ public async deleteSong(@Param('id') id: string, @GetRequestToken() user: UserDocument | null,): Promise {
user = validateUser(user);
await this.songService.deleteSong(id, user);
}
@@ -174,19 +227,10 @@ export class SongController {
@UseGuards(AuthGuard('jwt-refresh'))
@ApiBearerAuth()
@ApiConsumes('multipart/form-data')
- @ApiBody({
- description: 'Upload Song',
- type: UploadSongResponseDto,
- })
+ @ApiBody({ description: 'Upload Song', type: UploadSongResponseDto, })
@UseInterceptors(FileInterceptor('file', SongController.multerConfig))
- @ApiOperation({
- summary: 'Upload a .nbs file and send the song data, creating a new song',
- })
- public async createSong(
- @UploadedFile() file: Express.Multer.File,
- @Body() body: UploadSongDto,
- @GetRequestToken() user: UserDocument | null,
- ): Promise {
+ @ApiOperation({ summary: 'Upload a .nbs file and send the song data, creating a new song', })
+ public async createSong(@UploadedFile() file: Express.Multer.File, @Body() body: UploadSongDto, @GetRequestToken() user: UserDocument | null,): Promise {
user = validateUser(user);
return await this.songService.uploadSong({ body, file, user });
}
diff --git a/apps/backend/src/song/song.module.ts b/apps/backend/src/song/song.module.ts
index 52218af7..01b8a389 100644
--- a/apps/backend/src/song/song.module.ts
+++ b/apps/backend/src/song/song.module.ts
@@ -25,13 +25,13 @@ import { SongService } from './song.service';
SongUploadService,
SongWebhookService,
{
- inject: [ConfigService],
- provide: 'DISCORD_WEBHOOK_URL',
+ inject : [ConfigService],
+ provide : 'DISCORD_WEBHOOK_URL',
useFactory: (configService: ConfigService) =>
configService.getOrThrow('DISCORD_WEBHOOK_URL'),
},
],
controllers: [SongController, MySongsController],
- exports: [SongService],
+ exports : [SongService],
})
export class SongModule {}
diff --git a/apps/backend/src/song/song.service.spec.ts b/apps/backend/src/song/song.service.spec.ts
index b5445461..65e15eda 100644
--- a/apps/backend/src/song/song.service.spec.ts
+++ b/apps/backend/src/song/song.service.spec.ts
@@ -22,21 +22,21 @@ import { SongWebhookService } from './song-webhook/song-webhook.service';
import { SongService } from './song.service';
const mockFileService = {
- deleteSong: jest.fn(),
+ deleteSong : jest.fn(),
getSongDownloadUrl: jest.fn(),
};
const mockSongUploadService = {
processUploadedSong: jest.fn(),
- processSongPatch: jest.fn(),
+ processSongPatch : jest.fn(),
};
const mockSongWebhookService = {
syncAllSongsWebhook: jest.fn(),
- postSongWebhook: jest.fn(),
- updateSongWebhook: jest.fn(),
- deleteSongWebhook: jest.fn(),
- syncSongWebhook: jest.fn(),
+ postSongWebhook : jest.fn(),
+ updateSongWebhook : jest.fn(),
+ deleteSongWebhook : jest.fn(),
+ syncSongWebhook : jest.fn(),
};
describe('SongService', () => {
@@ -50,19 +50,19 @@ describe('SongService', () => {
providers: [
SongService,
{
- provide: SongWebhookService,
+ provide : SongWebhookService,
useValue: mockSongWebhookService,
},
{
- provide: getModelToken(SongEntity.name),
+ provide : getModelToken(SongEntity.name),
useValue: mongoose.model(SongEntity.name, SongSchema),
},
{
- provide: FileService,
+ provide : FileService,
useValue: mockFileService,
},
{
- provide: SongUploadService,
+ provide : SongUploadService,
useValue: mockSongUploadService,
},
],
@@ -84,53 +84,53 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const body: UploadSongDto = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
allowDownload: true,
- file: 'somebytes',
+ file : 'somebytes',
};
const commonData = {
- publicId: 'public-song-id',
+ publicId : 'public-song-id',
createdAt: new Date(),
- stats: {
- midiFileName: 'test.mid',
- noteCount: 100,
- tickCount: 1000,
- layerCount: 10,
- tempo: 120,
- tempoRange: [100, 150],
- timeSignature: 4,
- duration: 60,
- loop: true,
- loopStartTick: 0,
- minutesSpent: 10,
- vanillaInstrumentCount: 10,
- customInstrumentCount: 0,
+ stats : {
+ midiFileName : 'test.mid',
+ noteCount : 100,
+ tickCount : 1000,
+ layerCount : 10,
+ tempo : 120,
+ tempoRange : [100, 150],
+ timeSignature : 4,
+ duration : 60,
+ loop : true,
+ loopStartTick : 0,
+ minutesSpent : 10,
+ vanillaInstrumentCount : 10,
+ customInstrumentCount : 0,
firstCustomInstrumentIndex: 0,
- outOfRangeNoteCount: 0,
- detunedNoteCount: 0,
- customInstrumentNoteCount: 0,
- incompatibleNoteCount: 0,
- compatible: true,
- instrumentNoteCounts: [10],
+ outOfRangeNoteCount : 0,
+ detunedNoteCount : 0,
+ customInstrumentNoteCount : 0,
+ incompatibleNoteCount : 0,
+ compatible : true,
+ instrumentNoteCounts : [10],
},
- fileSize: 424242,
+ fileSize : 424242,
packedSongUrl: 'http://test.com/packed-file.nbs',
- nbsFileUrl: 'http://test.com/file.nbs',
- thumbnailUrl: 'http://test.com/thumbnail.nbs',
- uploader: user._id,
+ nbsFileUrl : 'http://test.com/file.nbs',
+ thumbnailUrl : 'http://test.com/thumbnail.nbs',
+ uploader : user._id,
};
const songEntity = new SongEntity();
@@ -189,29 +189,29 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const songEntity = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
allowDownload: true,
- file: 'somebytes',
- publicId: 'public-song-id',
- createdAt: new Date(),
- stats: {} as SongStats,
- fileSize: 424242,
+ file : 'somebytes',
+ publicId : 'public-song-id',
+ createdAt : new Date(),
+ stats : {} as SongStats,
+ fileSize : 424242,
packedSongUrl: 'http://test.com/packed-file.nbs',
- nbsFileUrl: 'http://test.com/file.nbs',
- thumbnailUrl: 'http://test.com/thumbnail.nbs',
- uploader: user._id,
+ nbsFileUrl : 'http://test.com/file.nbs',
+ thumbnailUrl : 'http://test.com/thumbnail.nbs',
+ uploader : user._id,
} as unknown as SongEntity;
const populatedSong = {
@@ -254,7 +254,7 @@ describe('SongService', () => {
const mockFindOne = {
findOne: jest.fn().mockReturnThis(),
- exec: jest.fn().mockResolvedValue(null),
+ exec : jest.fn().mockResolvedValue(null),
};
jest.spyOn(songModel, 'findOne').mockReturnValue(mockFindOne as any);
@@ -305,53 +305,53 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const body: UploadSongDto = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
allowDownload: true,
- file: 'somebytes',
+ file : 'somebytes',
};
const missingData = {
- publicId: 'public-song-id',
+ publicId : 'public-song-id',
createdAt: new Date(),
- stats: {
- midiFileName: 'test.mid',
- noteCount: 100,
- tickCount: 1000,
- layerCount: 10,
- tempo: 120,
- tempoRange: [100, 150],
- timeSignature: 4,
- duration: 60,
- loop: true,
- loopStartTick: 0,
- minutesSpent: 10,
- vanillaInstrumentCount: 10,
- customInstrumentCount: 0,
+ stats : {
+ midiFileName : 'test.mid',
+ noteCount : 100,
+ tickCount : 1000,
+ layerCount : 10,
+ tempo : 120,
+ tempoRange : [100, 150],
+ timeSignature : 4,
+ duration : 60,
+ loop : true,
+ loopStartTick : 0,
+ minutesSpent : 10,
+ vanillaInstrumentCount : 10,
+ customInstrumentCount : 0,
firstCustomInstrumentIndex: 0,
- outOfRangeNoteCount: 0,
- detunedNoteCount: 0,
- customInstrumentNoteCount: 0,
- incompatibleNoteCount: 0,
- compatible: true,
- instrumentNoteCounts: [10],
+ outOfRangeNoteCount : 0,
+ detunedNoteCount : 0,
+ customInstrumentNoteCount : 0,
+ incompatibleNoteCount : 0,
+ compatible : true,
+ instrumentNoteCounts : [10],
},
- fileSize: 424242,
+ fileSize : 424242,
packedSongUrl: 'http://test.com/packed-file.nbs',
- nbsFileUrl: 'http://test.com/file.nbs',
- thumbnailUrl: 'http://test.com/thumbnail.nbs',
- uploader: user._id,
+ nbsFileUrl : 'http://test.com/file.nbs',
+ thumbnailUrl : 'http://test.com/thumbnail.nbs',
+ uploader : user._id,
};
const songDocument: SongDocument = {
@@ -403,20 +403,20 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const body: UploadSongDto = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
- file: 'somebytes',
+ file : 'somebytes',
allowDownload: false,
};
@@ -432,20 +432,20 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const body: UploadSongDto = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
- file: 'somebytes',
+ file : 'somebytes',
allowDownload: false,
};
@@ -465,20 +465,20 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const body: UploadSongDto = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
- file: 'somebytes',
+ file : 'somebytes',
allowDownload: false,
};
@@ -498,39 +498,39 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const body: UploadSongDto = {
- file: undefined,
- allowDownload: false,
- visibility: 'public',
- title: '',
+ file : undefined,
+ allowDownload : false,
+ visibility : 'public',
+ title : '',
originalAuthor: '',
- description: '',
- category: 'pop',
- thumbnailData: {
+ description : '',
+ category : 'pop',
+ thumbnailData : {
backgroundColor: '#000000',
- startLayer: 0,
- startTick: 0,
- zoomLevel: 1,
+ startLayer : 0,
+ startTick : 0,
+ zoomLevel : 1,
},
- license: 'standard',
+ license : 'standard',
customInstruments: [],
};
const songEntity = {
- uploader: user._id,
- file: undefined,
- allowDownload: false,
- visibility: 'public',
- title: '',
+ uploader : user._id,
+ file : undefined,
+ allowDownload : false,
+ visibility : 'public',
+ title : '',
originalAuthor: '',
- description: '',
- category: 'pop',
- thumbnailData: {
+ description : '',
+ category : 'pop',
+ thumbnailData : {
backgroundColor: '#000000',
- startLayer: 0,
- startTick: 0,
- zoomLevel: 1,
+ startLayer : 0,
+ startTick : 0,
+ zoomLevel : 1,
},
- license: 'standard',
+ license : 'standard',
customInstruments: [],
} as any;
@@ -545,20 +545,20 @@ describe('SongService', () => {
describe('getSongByPage', () => {
it('should return a list of songs by page', async () => {
const query = {
- page: 1,
+ page : 1,
limit: 10,
- sort: 'createdAt',
+ sort : 'createdAt',
order: true,
};
const songList: SongWithUser[] = [];
const mockFind = {
- sort: jest.fn().mockReturnThis(),
- skip: jest.fn().mockReturnThis(),
- limit: jest.fn().mockReturnThis(),
+ sort : jest.fn().mockReturnThis(),
+ skip : jest.fn().mockReturnThis(),
+ limit : jest.fn().mockReturnThis(),
populate: jest.fn().mockReturnThis(),
- exec: jest.fn().mockResolvedValue(songList),
+ exec : jest.fn().mockResolvedValue(songList),
};
jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any);
@@ -582,20 +582,20 @@ describe('SongService', () => {
it('should throw an error if the query is invalid', async () => {
const query = {
- page: undefined,
+ page : undefined,
limit: undefined,
- sort: undefined,
+ sort : undefined,
order: true,
};
const songList: SongWithUser[] = [];
const mockFind = {
- sort: jest.fn().mockReturnThis(),
- skip: jest.fn().mockReturnThis(),
- limit: jest.fn().mockReturnThis(),
+ sort : jest.fn().mockReturnThis(),
+ skip : jest.fn().mockReturnThis(),
+ limit : jest.fn().mockReturnThis(),
populate: jest.fn().mockReturnThis(),
- exec: jest.fn().mockResolvedValue(songList),
+ exec : jest.fn().mockResolvedValue(songList),
};
jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any);
@@ -610,23 +610,23 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const songDocument = {
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
- file: 'somebytes',
+ file : 'somebytes',
allowDownload: false,
- uploader: {},
- save: jest.fn(),
+ uploader : {},
+ save : jest.fn(),
} as any;
songDocument.save = jest.fn().mockResolvedValue(songDocument);
@@ -665,9 +665,9 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const songEntity = {
- publicId: 'test-public-id',
+ publicId : 'test-public-id',
visibility: 'private',
- uploader: 'different-user-id',
+ uploader : 'different-user-id',
};
jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any);
@@ -680,9 +680,9 @@ describe('SongService', () => {
const user: UserDocument = null as any;
const songEntity = {
- publicId: 'test-public-id',
+ publicId : 'test-public-id',
visibility: 'private',
- uploader: 'different-user-id',
+ uploader : 'different-user-id',
};
jest.spyOn(songModel, 'findOne').mockReturnValue(songEntity as any);
@@ -696,29 +696,29 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const songEntity = {
- visibility: 'public',
- uploader: 'test-user-id',
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- license: 'standard',
+ visibility : 'public',
+ uploader : 'test-user-id',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
allowDownload: true,
- publicId: 'public-song-id',
- createdAt: new Date(),
- stats: {} as SongStats,
- fileSize: 424242,
+ publicId : 'public-song-id',
+ createdAt : new Date(),
+ stats : {} as SongStats,
+ fileSize : 424242,
packedSongUrl: 'http://test.com/packed-file.nbs',
- nbsFileUrl: 'http://test.com/file.nbs',
- thumbnailUrl: 'http://test.com/thumbnail.nbs',
- save: jest.fn(),
+ nbsFileUrl : 'http://test.com/file.nbs',
+ thumbnailUrl : 'http://test.com/thumbnail.nbs',
+ save : jest.fn(),
};
const url = 'http://test.com/song.nbs';
@@ -754,7 +754,7 @@ describe('SongService', () => {
const songEntity = {
visibility: 'private',
- uploader: 'different-user-id',
+ uploader : 'different-user-id',
};
jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity);
@@ -769,29 +769,29 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const songEntity = {
- visibility: 'public',
- uploader: 'test-user-id',
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- license: 'standard',
+ visibility : 'public',
+ uploader : 'test-user-id',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
allowDownload: false,
- publicId: 'public-song-id',
- createdAt: new Date(),
- stats: {} as SongStats,
- fileSize: 424242,
+ publicId : 'public-song-id',
+ createdAt : new Date(),
+ stats : {} as SongStats,
+ fileSize : 424242,
packedSongUrl: undefined,
- nbsFileUrl: 'http://test.com/file.nbs',
- thumbnailUrl: 'http://test.com/thumbnail.nbs',
- save: jest.fn(),
+ nbsFileUrl : 'http://test.com/file.nbs',
+ thumbnailUrl : 'http://test.com/thumbnail.nbs',
+ save : jest.fn(),
};
jest.spyOn(songModel, 'findOne').mockResolvedValue(songEntity);
@@ -821,29 +821,29 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const songEntity = {
- visibility: 'public',
- uploader: 'test-user-id',
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- license: 'standard',
+ visibility : 'public',
+ uploader : 'test-user-id',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
allowDownload: true,
- publicId: 'public-song-id',
- createdAt: new Date(),
- stats: {} as SongStats,
- fileSize: 424242,
+ publicId : 'public-song-id',
+ createdAt : new Date(),
+ stats : {} as SongStats,
+ fileSize : 424242,
packedSongUrl: 'http://test.com/packed-file.nbs',
- nbsFileUrl: 'http://test.com/file.nbs',
- thumbnailUrl: 'http://test.com/thumbnail.nbs',
- save: jest.fn().mockImplementationOnce(() => {
+ nbsFileUrl : 'http://test.com/file.nbs',
+ thumbnailUrl : 'http://test.com/thumbnail.nbs',
+ save : jest.fn().mockImplementationOnce(() => {
throw new Error('Error saving song');
}),
};
@@ -863,9 +863,9 @@ describe('SongService', () => {
describe('getMySongsPage', () => {
it('should return a list of songs uploaded by the user', async () => {
const query = {
- page: 1,
+ page : 1,
limit: 10,
- sort: 'createdAt',
+ sort : 'createdAt',
order: true,
};
@@ -873,8 +873,8 @@ describe('SongService', () => {
const songList: SongWithUser[] = [];
const mockFind = {
- sort: jest.fn().mockReturnThis(),
- skip: jest.fn().mockReturnThis(),
+ sort : jest.fn().mockReturnThis(),
+ skip : jest.fn().mockReturnThis(),
limit: jest.fn().mockResolvedValue(songList),
};
@@ -887,7 +887,7 @@ describe('SongService', () => {
content: songList.map((song) =>
SongPreviewDto.fromSongDocumentWithUser(song),
),
- page: 1,
+ page : 1,
limit: 10,
total: 0,
});
@@ -915,7 +915,7 @@ describe('SongService', () => {
songEntity.uploader = user._id; // Ensure uploader is set
const mockFindOne = {
- exec: jest.fn().mockResolvedValue(songEntity),
+ exec : jest.fn().mockResolvedValue(songEntity),
populate: jest.fn().mockReturnThis(),
};
@@ -934,7 +934,7 @@ describe('SongService', () => {
const findOneMock = {
findOne: jest.fn().mockReturnThis(),
- exec: jest.fn().mockResolvedValue(null),
+ exec : jest.fn().mockResolvedValue(null),
};
jest.spyOn(songModel, 'findOne').mockReturnValue(findOneMock as any);
@@ -949,33 +949,33 @@ describe('SongService', () => {
const user: UserDocument = { _id: 'test-user-id' } as UserDocument;
const songEntity = {
- uploader: 'different-user-id',
- title: 'Test Song',
- originalAuthor: 'Test Author',
- description: 'Test Description',
- category: 'alternative',
- visibility: 'public',
- license: 'standard',
+ uploader : 'different-user-id',
+ title : 'Test Song',
+ originalAuthor : 'Test Author',
+ description : 'Test Description',
+ category : 'alternative',
+ visibility : 'public',
+ license : 'standard',
customInstruments: [],
- thumbnailData: {
- startTick: 0,
- startLayer: 0,
- zoomLevel: 1,
+ thumbnailData : {
+ startTick : 0,
+ startLayer : 0,
+ zoomLevel : 1,
backgroundColor: '#000000',
},
allowDownload: true,
- publicId: 'public-song-id',
- createdAt: new Date(),
- stats: {} as SongStats,
- fileSize: 424242,
+ publicId : 'public-song-id',
+ createdAt : new Date(),
+ stats : {} as SongStats,
+ fileSize : 424242,
packedSongUrl: 'http://test.com/packed-file.nbs',
- nbsFileUrl: 'http://test.com/file.nbs',
- thumbnailUrl: 'http://test.com/thumbnail.nbs',
+ nbsFileUrl : 'http://test.com/file.nbs',
+ thumbnailUrl : 'http://test.com/thumbnail.nbs',
} as unknown as SongEntity;
const findOneMock = {
findOne: jest.fn().mockReturnThis(),
- exec: jest.fn().mockResolvedValue(songEntity),
+ exec : jest.fn().mockResolvedValue(songEntity),
};
jest.spyOn(songModel, 'findOne').mockReturnValue(findOneMock as any);
@@ -1015,11 +1015,11 @@ describe('SongService', () => {
const songList: SongWithUser[] = [];
const mockFind = {
- sort: jest.fn().mockReturnThis(),
- skip: jest.fn().mockReturnThis(),
- limit: jest.fn().mockReturnThis(),
+ sort : jest.fn().mockReturnThis(),
+ skip : jest.fn().mockReturnThis(),
+ limit : jest.fn().mockReturnThis(),
populate: jest.fn().mockReturnThis(),
- exec: jest.fn().mockResolvedValue(songList),
+ exec : jest.fn().mockResolvedValue(songList),
};
jest.spyOn(songModel, 'find').mockReturnValue(mockFind as any);
@@ -1047,4 +1047,70 @@ describe('SongService', () => {
expect(mockFind.exec).toHaveBeenCalled();
});
});
+
+ describe('getFeaturedSongs', () => {
+ it('should return featured songs', async () => {
+ const songWithUser: SongWithUser = {
+ title : 'Test Song',
+ publicId : 'test-id',
+ uploader : { username: 'testuser', profileImage: 'testimage' },
+ description : 'Test Description',
+ originalAuthor: 'Test Author',
+ stats : {
+ duration : 100,
+ noteCount: 100,
+ },
+ thumbnailUrl: 'test-thumbnail-url',
+ createdAt : new Date(),
+ updatedAt : new Date(),
+ playCount : 0,
+ visibility : 'public',
+ } as any;
+
+ jest
+ .spyOn(service, 'getSongsForTimespan')
+ .mockResolvedValue([songWithUser]);
+
+ jest
+ .spyOn(service, 'getSongsBeforeTimespan')
+ .mockResolvedValue([]);
+
+ const result = await service.getFeaturedSongs();
+
+ expect(service.getSongsForTimespan).toHaveBeenCalledTimes(6); // Called for each timespan
+ expect(result).toBeInstanceOf(Object);
+ expect(result).toHaveProperty('hour');
+ expect(result).toHaveProperty('day');
+ expect(result).toHaveProperty('week');
+ expect(result).toHaveProperty('month');
+ expect(result).toHaveProperty('year');
+ expect(result).toHaveProperty('all');
+ expect(Array.isArray(result.hour)).toBe(true);
+ expect(Array.isArray(result.day)).toBe(true);
+ expect(Array.isArray(result.week)).toBe(true);
+ expect(Array.isArray(result.month)).toBe(true);
+ expect(Array.isArray(result.year)).toBe(true);
+ expect(Array.isArray(result.all)).toBe(true);
+ });
+
+ it('should handle empty results gracefully', async () => {
+ jest
+ .spyOn(service, 'getSongsForTimespan')
+ .mockResolvedValue([]);
+
+ jest
+ .spyOn(service, 'getSongsBeforeTimespan')
+ .mockResolvedValue([]);
+
+ const result = await service.getFeaturedSongs();
+
+ expect(result).toBeInstanceOf(Object);
+ expect(result.hour).toEqual([]);
+ expect(result.day).toEqual([]);
+ expect(result.week).toEqual([]);
+ expect(result.month).toEqual([]);
+ expect(result.year).toEqual([]);
+ expect(result.all).toEqual([]);
+ });
+ });
});
diff --git a/apps/backend/src/song/song.service.ts b/apps/backend/src/song/song.service.ts
index 1da06c08..89b20128 100644
--- a/apps/backend/src/song/song.service.ts
+++ b/apps/backend/src/song/song.service.ts
@@ -1,22 +1,6 @@
import { BROWSER_SONGS } from '@nbw/config';
-import type { UserDocument } from '@nbw/database';
-import {
- PageQueryDTO,
- Song as SongEntity,
- SongPageDto,
- SongPreviewDto,
- SongViewDto,
- SongWithUser,
- UploadSongDto,
- UploadSongResponseDto,
-} from '@nbw/database';
-import {
- HttpException,
- HttpStatus,
- Inject,
- Injectable,
- Logger,
-} from '@nestjs/common';
+import { FeaturedSongsDto, TimespanType, UserDocument, PageQueryDTO, Song as SongEntity, SongPageDto, SongPreviewDto, SongViewDto, SongWithUser, UploadSongDto, UploadSongResponseDto, } from '@nbw/database';
+import { HttpException, HttpStatus, Inject, Injectable, Logger, } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@@ -49,15 +33,7 @@ export class SongService {
});
}
- public async uploadSong({
- file,
- user,
- body,
- }: {
- body: UploadSongDto;
- file: Express.Multer.File;
- user: UserDocument;
- }): Promise {
+ public async uploadSong({ file, user, body, }: { body: UploadSongDto; file: Express.Multer.File; user: UserDocument;}): Promise {
const song = await this.songUploadService.processUploadedSong({
file,
user,
@@ -85,10 +61,7 @@ export class SongService {
return UploadSongResponseDto.fromSongWithUserDocument(populatedSong);
}
- public async deleteSong(
- publicId: string,
- user: UserDocument,
- ): Promise {
+ public async deleteSong(publicId: string, user: UserDocument,): Promise {
const foundSong = await this.songModel
.findOne({ publicId: publicId })
.exec();
@@ -115,11 +88,7 @@ export class SongService {
return UploadSongResponseDto.fromSongWithUserDocument(populatedSong);
}
- public async patchSong(
- publicId: string,
- body: UploadSongDto,
- user: UserDocument,
- ): Promise {
+ public async patchSong(publicId: string, body: UploadSongDto, user: UserDocument,): Promise {
const foundSong = await this.songModel.findOne({
publicId: publicId,
});
@@ -202,16 +171,52 @@ 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[];
return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song));
}
- public async getRecentSongs(
- page: number,
- limit: number,
- ): Promise {
+ public async getRecentSongs(page: number, limit: number,): Promise {
const queryObject: any = {
visibility: 'public',
};
@@ -233,7 +238,7 @@ export class SongService {
return this.songModel
.find({
visibility: 'public',
- createdAt: {
+ createdAt : {
$gte: timespan,
},
})
@@ -243,13 +248,11 @@ export class SongService {
.exec();
}
- public async getSongsBeforeTimespan(
- timespan: number,
- ): Promise {
+ public async getSongsBeforeTimespan(timespan: number,): Promise {
return this.songModel
.find({
visibility: 'public',
- createdAt: {
+ createdAt : {
$lt: timespan,
},
})
@@ -259,10 +262,7 @@ export class SongService {
.exec();
}
- public async getSong(
- publicId: string,
- user: UserDocument | null,
- ): Promise {
+ public async getSong(publicId: string, user: UserDocument | null,): Promise {
const foundSong = await this.songModel.findOne({ publicId: publicId });
if (!foundSong) {
@@ -292,12 +292,7 @@ export class SongService {
}
// TODO: service should not handle HTTP -> https://www.reddit.com/r/node/comments/uoicw1/should_i_return_status_code_from_service_layer/
- public async getSongDownloadUrl(
- publicId: string,
- user: UserDocument | null,
- src?: string,
- packed: boolean = false,
- ): Promise {
+ public async getSongDownloadUrl(publicId: string, user: UserDocument | null, src?: string, packed: boolean = false,): Promise {
const foundSong = await this.songModel.findOne({ publicId: publicId });
if (!foundSong) {
@@ -342,13 +337,7 @@ export class SongService {
}
}
- public async getMySongsPage({
- query,
- user,
- }: {
- query: PageQueryDTO;
- user: UserDocument;
- }): Promise {
+ public async getMySongsPage({ query, user, }: { query: PageQueryDTO; user: UserDocument;}): Promise {
const page = parseInt(query.page?.toString() ?? '1');
const limit = parseInt(query.limit?.toString() ?? '10');
const order = query.order ? query.order : false;
@@ -372,16 +361,13 @@ export class SongService {
content: songData.map((song) =>
SongPreviewDto.fromSongDocumentWithUser(song),
),
- page: page,
+ page : page,
limit: limit,
total: total,
};
}
- public async getSongEdit(
- publicId: string,
- user: UserDocument,
- ): Promise {
+ public async getSongEdit(publicId: string, user: UserDocument,): Promise {
const foundSong = await this.songModel
.findOne({ publicId: publicId })
.exec();
@@ -408,7 +394,7 @@ export class SongService {
},
{
$group: {
- _id: '$category',
+ _id : '$category',
count: { $sum: 1 },
},
},
@@ -429,14 +415,10 @@ export class SongService {
}, {} as Record);
}
- public async getSongsByCategory(
- category: string,
- page: number,
- limit: number,
- ): Promise {
+ public async getSongsByCategory(category: string, page: number, limit: number,): Promise {
const songs = (await this.songModel
.find({
- category: category,
+ category : category,
visibility: 'public',
})
.sort({ createdAt: -1 })
@@ -448,15 +430,13 @@ export class SongService {
return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song));
}
- public async getRandomSongs(
- count: number,
- category: string,
- ): Promise {
+ public async getRandomSongs(count: number, category: string,): Promise {
const songs = (await this.songModel
.aggregate([
{
$match: {
visibility: 'public',
+ category : category,
},
},
{
@@ -468,14 +448,58 @@ export class SongService {
.exec()) as unknown as SongWithUser[];
await this.songModel.populate(songs, {
- path: 'uploader',
+ path : 'uploader',
select: 'username profileImage -_id',
});
return songs.map((song) => SongPreviewDto.fromSongDocumentWithUser(song));
}
- public async getAllSongs() {
- return this.songModel.find({});
+ public async getFeaturedSongs(): Promise {
+ const now = new Date(Date.now());
+
+ const times: Record = {
+ hour : new Date(Date.now()).setHours(now.getHours() - 1),
+ day : new Date(Date.now()).setDate(now.getDate() - 1),
+ week : new Date(Date.now()).setDate(now.getDate() - 7),
+ month: new Date(Date.now()).setMonth(now.getMonth() - 1),
+ year : new Date(Date.now()).setFullYear(now.getFullYear() - 1),
+ all : new Date(0).getTime(),
+ };
+
+ const songs: Record = { hour: [], day: [], week: [], month: [], year: [], all: [], };
+
+ for (const [timespan, time] of Object.entries(times)) {
+ const songPage = await this.getSongsForTimespan(time);
+
+ // If the length is 0, send an empty array (no songs available in that timespan)
+ // If the length is less than the page size, pad it with songs "borrowed"
+ // from the nearest timestamp, regardless of view count
+ if (
+ songPage.length > 0 &&
+ songPage.length < BROWSER_SONGS.paddedFeaturedPageSize
+ ) {
+ const missing = BROWSER_SONGS.paddedFeaturedPageSize - songPage.length;
+
+ const additionalSongs = await this.getSongsBeforeTimespan(
+ time,
+ );
+
+ songPage.push(...additionalSongs.slice(0, missing));
+ }
+
+ songs[timespan as TimespanType] = songPage;
+ }
+
+ const featuredSongs = FeaturedSongsDto.create();
+
+ featuredSongs.hour = songs.hour.map((song) => SongPreviewDto.fromSongDocumentWithUser(song),);
+ featuredSongs.day = songs.day.map((song) => SongPreviewDto.fromSongDocumentWithUser(song),);
+ featuredSongs.week = songs.week.map((song) => SongPreviewDto.fromSongDocumentWithUser(song),);
+ featuredSongs.month = songs.month.map((song) => SongPreviewDto.fromSongDocumentWithUser(song),);
+ featuredSongs.year = songs.year.map((song) => SongPreviewDto.fromSongDocumentWithUser(song),);
+ featuredSongs.all = songs.all.map((song) => SongPreviewDto.fromSongDocumentWithUser(song),);
+
+ return featuredSongs;
}
}
diff --git a/apps/backend/src/song/song.util.ts b/apps/backend/src/song/song.util.ts
index 50168171..0b38e2a3 100644
--- a/apps/backend/src/song/song.util.ts
+++ b/apps/backend/src/song/song.util.ts
@@ -46,26 +46,26 @@ export function getUploadDiscordEmbed({
if (originalAuthor) {
fieldsArray.push({
- name: 'Original Author',
- value: originalAuthor,
+ name : 'Original Author',
+ value : originalAuthor,
inline: false,
});
}
fieldsArray = fieldsArray.concat([
{
- name: 'Category',
- value: UPLOAD_CONSTANTS.categories[category],
+ name : 'Category',
+ value : UPLOAD_CONSTANTS.categories[category],
inline: true,
},
{
- name: 'Notes',
- value: stats.noteCount.toLocaleString('en-US'),
+ name : 'Notes',
+ value : stats.noteCount.toLocaleString('en-US'),
inline: true,
},
{
- name: 'Length',
- value: formatDuration(stats.duration),
+ name : 'Length',
+ value : formatDuration(stats.duration),
inline: true,
},
]);
@@ -73,23 +73,23 @@ export function getUploadDiscordEmbed({
return {
embeds: [
{
- title: title,
+ title : title,
description: description,
- color: Number('0x' + thumbnailData.backgroundColor.replace('#', '')),
- timestamp: createdAt.toISOString(),
- footer: {
+ color : Number('0x' + thumbnailData.backgroundColor.replace('#', '')),
+ timestamp : createdAt.toISOString(),
+ footer : {
text: UPLOAD_CONSTANTS.licenses[license]
? UPLOAD_CONSTANTS.licenses[license].shortName
: 'Unknown License',
},
author: {
- name: uploader.username,
+ name : uploader.username,
icon_url: uploader.profileImage,
//url: 'https://noteblock.world/user/${uploaderName}',
},
fields: fieldsArray,
- url: `https://noteblock.world/song/${publicId}`,
- image: {
+ url : `https://noteblock.world/song/${publicId}`,
+ image : {
url: thumbnailUrl,
},
thumbnail: {
diff --git a/apps/backend/src/user/user.controller.spec.ts b/apps/backend/src/user/user.controller.spec.ts
index b8b94b96..4ff82173 100644
--- a/apps/backend/src/user/user.controller.spec.ts
+++ b/apps/backend/src/user/user.controller.spec.ts
@@ -8,8 +8,8 @@ import { UserService } from './user.service';
const mockUserService = {
getUserByEmailOrId: jest.fn(),
- getUserPaginated: jest.fn(),
- getSelfUserData: jest.fn(),
+ getUserPaginated : jest.fn(),
+ getSelfUserData : jest.fn(),
};
describe('UserController', () => {
@@ -19,9 +19,9 @@ describe('UserController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
- providers: [
+ providers : [
{
- provide: UserService,
+ provide : UserService,
useValue: mockUserService,
},
],
@@ -38,9 +38,9 @@ describe('UserController', () => {
describe('getUser', () => {
it('should return user data by email or ID', async () => {
const query: GetUser = {
- email: 'test@email.com',
+ email : 'test@email.com',
username: 'test-username',
- id: 'test-id',
+ id : 'test-id',
};
const user = { email: 'test@example.com' };
diff --git a/apps/backend/src/user/user.module.ts b/apps/backend/src/user/user.module.ts
index 53a58c90..6c5cb045 100644
--- a/apps/backend/src/user/user.module.ts
+++ b/apps/backend/src/user/user.module.ts
@@ -9,8 +9,8 @@ import { UserService } from './user.service';
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
- providers: [UserService],
+ providers : [UserService],
controllers: [UserController],
- exports: [UserService],
+ exports : [UserService],
})
export class UserModule {}
diff --git a/apps/backend/src/user/user.service.spec.ts b/apps/backend/src/user/user.service.spec.ts
index 56e07464..54e558c4 100644
--- a/apps/backend/src/user/user.service.spec.ts
+++ b/apps/backend/src/user/user.service.spec.ts
@@ -13,13 +13,13 @@ import { Model } from 'mongoose';
import { UserService } from './user.service';
const mockUserModel = {
- create: jest.fn(),
- findOne: jest.fn(),
- findById: jest.fn(),
- find: jest.fn(),
- save: jest.fn(),
- exec: jest.fn(),
- select: jest.fn(),
+ create : jest.fn(),
+ findOne : jest.fn(),
+ findById : jest.fn(),
+ find : jest.fn(),
+ save : jest.fn(),
+ exec : jest.fn(),
+ select : jest.fn(),
countDocuments: jest.fn(),
};
@@ -32,7 +32,7 @@ describe('UserService', () => {
providers: [
UserService,
{
- provide: getModelToken(User.name),
+ provide : getModelToken(User.name),
useValue: mockUserModel,
},
],
@@ -49,8 +49,8 @@ describe('UserService', () => {
describe('create', () => {
it('should create a new user', async () => {
const createUserDto: CreateUser = {
- username: 'testuser',
- email: 'test@example.com',
+ username : 'testuser',
+ email : 'test@example.com',
profileImage: 'testimage.png',
};
@@ -109,13 +109,13 @@ describe('UserService', () => {
const usersPage = {
users,
total: 1,
- page: 1,
+ page : 1,
limit: 10,
};
const mockFind = {
- sort: jest.fn().mockReturnThis(),
- skip: jest.fn().mockReturnThis(),
+ sort : jest.fn().mockReturnThis(),
+ skip : jest.fn().mockReturnThis(),
limit: jest.fn().mockResolvedValue(users),
};
@@ -184,7 +184,7 @@ describe('UserService', () => {
jest.spyOn(userModel, 'findById').mockReturnValue({
populate: jest.fn().mockReturnThis(),
- exec: jest.fn().mockResolvedValue(hydratedUser),
+ exec : jest.fn().mockResolvedValue(hydratedUser),
} as any);
const result = await service.getHydratedUser(user);
@@ -229,9 +229,9 @@ describe('UserService', () => {
const userData = {
...user,
- lastSeen: yesterday,
+ lastSeen : yesterday,
loginStreak: 1,
- save: jest.fn().mockResolvedValue(true),
+ save : jest.fn().mockResolvedValue(true),
} as unknown as UserDocument;
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
@@ -250,9 +250,9 @@ describe('UserService', () => {
const userData = {
...user,
- lastSeen: today,
+ lastSeen : today,
loginStreak: 1,
- save: jest.fn().mockResolvedValue(true),
+ save : jest.fn().mockResolvedValue(true),
} as unknown as UserDocument;
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
@@ -272,9 +272,9 @@ describe('UserService', () => {
const userData = {
...user,
- lastSeen: twoDaysAgo,
+ lastSeen : twoDaysAgo,
loginStreak: 5,
- save: jest.fn().mockResolvedValue(true),
+ save : jest.fn().mockResolvedValue(true),
} as unknown as UserDocument;
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
@@ -294,9 +294,9 @@ describe('UserService', () => {
const userData = {
...user,
- lastSeen: yesterday,
+ lastSeen : yesterday,
loginCount: 5,
- save: jest.fn().mockResolvedValue(true),
+ save : jest.fn().mockResolvedValue(true),
} as unknown as UserDocument;
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
@@ -316,9 +316,9 @@ describe('UserService', () => {
const userData = {
...user,
- lastSeen: today,
+ lastSeen : today,
loginCount: 5,
- save: jest.fn().mockResolvedValue(true),
+ save : jest.fn().mockResolvedValue(true),
} as unknown as UserDocument;
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
@@ -339,10 +339,10 @@ describe('UserService', () => {
const userData = {
...user,
- lastSeen: yesterday,
- loginStreak: 8,
+ lastSeen : yesterday,
+ loginStreak : 8,
maxLoginStreak: 8,
- save: jest.fn().mockResolvedValue(true),
+ save : jest.fn().mockResolvedValue(true),
} as unknown as UserDocument;
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
@@ -362,10 +362,10 @@ describe('UserService', () => {
const userData = {
...user,
- lastSeen: yesterday,
- loginStreak: 4,
+ lastSeen : yesterday,
+ loginStreak : 4,
maxLoginStreak: 8,
- save: jest.fn().mockResolvedValue(true),
+ save : jest.fn().mockResolvedValue(true),
} as unknown as UserDocument;
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
@@ -384,7 +384,7 @@ describe('UserService', () => {
jest.spyOn(userModel, 'findOne').mockReturnValue({
select: jest.fn().mockReturnThis(),
- exec: jest.fn().mockResolvedValue(user),
+ exec : jest.fn().mockResolvedValue(user),
} as any);
const result = await service.usernameExists(username);
@@ -402,7 +402,7 @@ describe('UserService', () => {
jest.spyOn(userModel, 'findOne').mockReturnValue({
select: jest.fn().mockReturnThis(),
- exec: jest.fn().mockResolvedValue(null),
+ exec : jest.fn().mockResolvedValue(null),
} as any);
const result = await service.usernameExists(username);
@@ -488,7 +488,7 @@ describe('UserService', () => {
it('should update a user username', async () => {
const user = {
username: 'testuser',
- save: jest.fn().mockReturnThis(),
+ save : jest.fn().mockReturnThis(),
} as unknown as UserDocument;
const body = { username: 'newuser' };
@@ -498,9 +498,9 @@ describe('UserService', () => {
const result = await service.updateUsername(user, body);
expect(result).toEqual({
- username: 'newuser',
+ username : 'newuser',
publicName: undefined,
- email: undefined,
+ email : undefined,
});
expect(user.username).toBe(body.username);
@@ -510,7 +510,7 @@ describe('UserService', () => {
it('should throw an error if username already exists', async () => {
const user = {
username: 'testuser',
- save: jest.fn().mockReturnThis(),
+ save : jest.fn().mockReturnThis(),
} as unknown as UserDocument;
const body = { username: 'newuser' };
diff --git a/apps/backend/src/user/user.service.ts b/apps/backend/src/user/user.service.ts
index 051da77e..c83152ee 100644
--- a/apps/backend/src/user/user.service.ts
+++ b/apps/backend/src/user/user.service.ts
@@ -56,8 +56,8 @@ export class UserService {
);
const user = await this.userModel.create({
- email: email,
- username: emailPrefixUsername,
+ email : email,
+ username : emailPrefixUsername,
publicName: emailPrefixUsername,
});
diff --git a/apps/frontend/mdx-components.tsx b/apps/frontend/mdx-components.tsx
index 92af1be8..b48713a4 100644
--- a/apps/frontend/mdx-components.tsx
+++ b/apps/frontend/mdx-components.tsx
@@ -1,40 +1,8 @@
import type { MDXComponents } from 'mdx/types';
-import {
- a,
- blockquote,
- code,
- h1,
- h2,
- h3,
- h4,
- h5,
- h6,
- hr,
- li,
- ol,
- p,
- pre,
- ul,
-} from '@web/modules/shared/components/CustomMarkdown';
+import { a, blockquote, code, h1, h2, h3, h4, h5, h6, hr, li, ol, p, pre, ul, } from '@web/modules/shared/components/CustomMarkdown';
+
export function useMDXComponents(components: MDXComponents): MDXComponents {
- return {
- p,
- h1,
- h2,
- h3,
- h4,
- h5,
- h6,
- hr,
- ul,
- ol,
- li,
- blockquote,
- pre,
- code,
- a,
- ...components,
- };
+ return { p, h1, h2, h3, h4, h5, h6, hr, ul, ol, li, blockquote, pre, code, a, ...components, };
}
diff --git a/apps/frontend/src/app/(content)/(info)/about/page.tsx b/apps/frontend/src/app/(content)/(info)/about/page.tsx
index 37916161..2190abd5 100644
--- a/apps/frontend/src/app/(content)/(info)/about/page.tsx
+++ b/apps/frontend/src/app/(content)/(info)/about/page.tsx
@@ -1,3 +1,4 @@
+
import type { Metadata } from 'next';
import BackButton from '@web/modules/shared/components/client/BackButton';
diff --git a/apps/frontend/src/app/(content)/(info)/blog/[id]/page.tsx b/apps/frontend/src/app/(content)/(info)/blog/[id]/page.tsx
index c0d63d05..7df6c08b 100644
--- a/apps/frontend/src/app/(content)/(info)/blog/[id]/page.tsx
+++ b/apps/frontend/src/app/(content)/(info)/blog/[id]/page.tsx
@@ -6,6 +6,7 @@ import { notFound } from 'next/navigation';
import { PostType, getPostData } from '@web/lib/posts';
import { CustomMarkdown } from '@web/modules/shared/components/CustomMarkdown';
+
type BlogPageProps = {
params: { id: string };
};
@@ -17,13 +18,13 @@ export function generateMetadata({ params }: BlogPageProps): Metadata {
const publicUrl = process.env.NEXT_PUBLIC_URL;
return {
- title: post.title,
- authors: [{ name: post.author }],
+ title : post.title,
+ authors : [{ name: post.author }],
openGraph: {
- url: publicUrl + '/blog/' + id,
- title: post.title,
+ url : publicUrl + '/blog/' + id,
+ title : post.title,
siteName: 'Note Block World',
- images: [
+ images : [
{
url: publicUrl + post.image,
},
@@ -82,9 +83,9 @@ const BlogPost = ({ params }: BlogPageProps) => {
{new Date(
new Date(post.date).getTime() + 12 * 60 * 60 * 1000,
).toLocaleDateString('en-UK', {
- day: 'numeric',
+ day : 'numeric',
month: 'short',
- year: 'numeric',
+ year : 'numeric',
})}
diff --git a/apps/frontend/src/app/(content)/(info)/blog/page.tsx b/apps/frontend/src/app/(content)/(info)/blog/page.tsx
index bcadb92c..23773d07 100644
--- a/apps/frontend/src/app/(content)/(info)/blog/page.tsx
+++ b/apps/frontend/src/app/(content)/(info)/blog/page.tsx
@@ -4,8 +4,9 @@ import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
-import { getSortedPostsData } from '@web/lib/posts';
import type { PostType } from '@web/lib/posts';
+import { getSortedPostsData } from '@web/lib/posts';
+
export const metadata: Metadata = {
title: 'Blog',
diff --git a/apps/frontend/src/app/(content)/(info)/contact/page.tsx b/apps/frontend/src/app/(content)/(info)/contact/page.tsx
index ebd20a1a..d52ecdf5 100644
--- a/apps/frontend/src/app/(content)/(info)/contact/page.tsx
+++ b/apps/frontend/src/app/(content)/(info)/contact/page.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from 'next';
import BackButton from '@web/modules/shared/components/client/BackButton';
+
import Contact from './contact.mdx';
export const metadata: Metadata = {
diff --git a/apps/frontend/src/app/(content)/(info)/help/[id]/page.tsx b/apps/frontend/src/app/(content)/(info)/help/[id]/page.tsx
index 2507590c..05d90e4f 100644
--- a/apps/frontend/src/app/(content)/(info)/help/[id]/page.tsx
+++ b/apps/frontend/src/app/(content)/(info)/help/[id]/page.tsx
@@ -6,6 +6,7 @@ import { notFound } from 'next/navigation';
import { PostType, getPostData } from '@web/lib/posts';
import { CustomMarkdown } from '@web/modules/shared/components/CustomMarkdown';
+
type HelpPageProps = {
params: { id: string };
};
@@ -17,13 +18,13 @@ export function generateMetadata({ params }: HelpPageProps): Metadata {
const publicUrl = process.env.NEXT_PUBLIC_URL;
return {
- title: post.title,
- authors: [{ name: post.author }],
+ title : post.title,
+ authors : [{ name: post.author }],
openGraph: {
- url: publicUrl + '/help/' + id,
- title: post.title,
+ url : publicUrl + '/help/' + id,
+ title : post.title,
siteName: 'Note Block World',
- images: [
+ images : [
{
url: publicUrl + post.image,
},
diff --git a/apps/frontend/src/app/(content)/(info)/help/page.tsx b/apps/frontend/src/app/(content)/(info)/help/page.tsx
index f0984c24..8a57b647 100644
--- a/apps/frontend/src/app/(content)/(info)/help/page.tsx
+++ b/apps/frontend/src/app/(content)/(info)/help/page.tsx
@@ -4,8 +4,9 @@ import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
-import { getSortedPostsData } from '@web/lib/posts';
import type { PostType } from '@web/lib/posts';
+import { getSortedPostsData } from '@web/lib/posts';
+
export const metadata: Metadata = {
title: 'Help Center',
diff --git a/apps/frontend/src/app/(content)/my-songs/page.tsx b/apps/frontend/src/app/(content)/my-songs/page.tsx
index 110df923..9fca9350 100644
--- a/apps/frontend/src/app/(content)/my-songs/page.tsx
+++ b/apps/frontend/src/app/(content)/my-songs/page.tsx
@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
import { checkLogin } from '@web/modules/auth/features/auth.utils';
import Page from '@web/modules/my-songs/components/MySongsPage';
+
export const metadata: Metadata = {
title: 'My songs',
};
diff --git a/apps/frontend/src/app/(content)/page.tsx b/apps/frontend/src/app/(content)/page.tsx
index ac9ee47c..45b15807 100644
--- a/apps/frontend/src/app/(content)/page.tsx
+++ b/apps/frontend/src/app/(content)/page.tsx
@@ -8,12 +8,13 @@ import { HomePageComponent } from '@web/modules/browse/components/HomePageCompon
async function fetchRecentSongs() {
try {
const response = await axiosInstance.get(
- '/song-browser/recent',
+ '/song',
{
params: {
- page: 1, // TODO: fiz constants
+ q : 'recent',
+ page : 1, // TODO: fiz constants
limit: 16, // TODO: change 'limit' parameter to 'skip' and load 12 songs initially, then load 8 more songs on each pagination
- sort: 'recent',
+ sort : 'recent',
order: false,
},
},
@@ -28,18 +29,18 @@ async function fetchRecentSongs() {
async function fetchFeaturedSongs(): Promise {
try {
const response = await axiosInstance.get(
- '/song-browser/featured',
+ '/song?q=featured',
);
return response.data;
} catch (error) {
return {
- hour: [],
- day: [],
- week: [],
+ hour : [],
+ day : [],
+ week : [],
month: [],
- year: [],
- all: [],
+ year : [],
+ all : [],
};
}
}
diff --git a/apps/frontend/src/app/(content)/search-song/page.tsx b/apps/frontend/src/app/(content)/search-song/page.tsx
new file mode 100644
index 00000000..93e1c9a5
--- /dev/null
+++ b/apps/frontend/src/app/(content)/search-song/page.tsx
@@ -0,0 +1,206 @@
+'use client';
+
+import {
+ faEllipsis,
+ faMagnifyingGlass,
+} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { SongPreviewDtoType } from '@nbw/database';
+import { useSearchParams } from 'next/navigation';
+import { useEffect, useState } from 'react';
+
+import LoadMoreButton from '@web/modules/browse/components/client/LoadMoreButton';
+import SongCard from '@web/modules/browse/components/SongCard';
+import SongCardGroup from '@web/modules/browse/components/SongCardGroup';
+
+// Mock data for testing
+const mockSongs: SongPreviewDtoType[] = [
+ {
+ publicId: '1',
+ uploader: {
+ username : 'musicmaker',
+ profileImage: '/img/note-block-pfp.jpg',
+ },
+ title : 'Beautiful Melody',
+ description : 'A peaceful song for relaxation',
+ originalAuthor: 'John Doe',
+ duration : 180,
+ noteCount : 150,
+ thumbnailUrl : '/img/note-block-grayscale.png',
+ createdAt : new Date('2024-01-15'),
+ updatedAt : new Date('2024-01-15'),
+ playCount : 1245,
+ visibility : 'public',
+ },
+ {
+ publicId: '2',
+ uploader: {
+ username : 'composer123',
+ profileImage: '/img/note-block-pfp.jpg',
+ },
+ title : 'Epic Adventure Theme',
+ description : 'An exciting soundtrack for your adventures',
+ originalAuthor: 'Jane Smith',
+ duration : 240,
+ noteCount : 320,
+ thumbnailUrl : '/img/note-block-grayscale.png',
+ createdAt : new Date('2024-01-14'),
+ updatedAt : new Date('2024-01-14'),
+ playCount : 856,
+ visibility : 'public',
+ },
+ {
+ publicId: '3',
+ uploader: {
+ username : 'beatmaster',
+ profileImage: '/img/note-block-pfp.jpg',
+ },
+ title : 'Minecraft Nostalgia',
+ description : 'Classic minecraft-inspired music',
+ originalAuthor: 'C418',
+ duration : 195,
+ noteCount : 280,
+ thumbnailUrl : '/img/note-block-grayscale.png',
+ createdAt : new Date('2024-01-13'),
+ updatedAt : new Date('2024-01-13'),
+ playCount : 2134,
+ visibility : 'public',
+ },
+];
+
+const SearchSongPage = () => {
+ const searchParams = useSearchParams();
+ const query = searchParams.get('q') || '';
+ const page = parseInt(searchParams.get('page') || '1');
+ const limit = parseInt(searchParams.get('limit') || '20');
+
+ const [songs, setSongs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [hasMore, setHasMore] = useState(true);
+ const [currentPage, setCurrentPage] = useState(page);
+
+ // Mock search function
+ const searchSongs = (searchQuery: string, pageNum: number) => {
+ setLoading(true);
+
+ // Simulate API call delay
+ setTimeout(() => {
+ if (pageNum === 1) {
+ // Filter mock songs based on query
+ const filtered = mockSongs.filter(
+ (song) =>
+ song.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ song.description
+ .toLowerCase()
+ .includes(searchQuery.toLowerCase()) ||
+ song.originalAuthor
+ .toLowerCase()
+ .includes(searchQuery.toLowerCase()) ||
+ song.uploader.username
+ .toLowerCase()
+ .includes(searchQuery.toLowerCase()),
+ );
+ setSongs(filtered);
+ setHasMore(filtered.length >= limit);
+ } else {
+ // For pagination, just add duplicates with modified IDs for demo
+ const additionalSongs = mockSongs.slice(0, 2).map((song, index) => ({
+ ...song,
+ publicId: `${song.publicId}-page-${pageNum}-${index}`,
+ title : `${song.title} (Page ${pageNum})`,
+ }));
+ setSongs((prev) => [...prev, ...additionalSongs]);
+ setHasMore(pageNum < 3); // Mock: only show 3 pages max
+ }
+ setLoading(false);
+ }, 500);
+ };
+
+ const loadMore = () => {
+ const nextPage = currentPage + 1;
+ setCurrentPage(nextPage);
+ searchSongs(query, nextPage);
+ };
+
+ useEffect(() => {
+ setCurrentPage(page);
+ searchSongs(query, page);
+ }, [query, page]);
+
+ if (loading && songs.length === 0) {
+ return (
+
+
+
+
Searching...
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+ {/* Search header */}
+
+
+
+
Search Results
+ {query && (
+
+ {songs.length > 0
+ ? `Found ${songs.length} songs for "${query}"`
+ : `No songs found for "${query}"`}
+
+ )}
+
+
+
+ {/* Results */}
+ {songs.length > 0 ? (
+ <>
+
+ {songs.map((song, i) => (
+
+ ))}
+
+
+ {/* Load more / End indicator */}
+
+ {loading ? (
+
Loading more songs...
+ ) : hasMore ? (
+
+ ) : (
+
+ )}
+
+ >
+ ) : !loading ? (
+
+
+
No songs found
+
+ Try adjusting your search terms or browse our featured songs
+ instead.
+
+
+ ) : null}
+
+ );
+};
+
+export default SearchSongPage;
diff --git a/apps/frontend/src/app/(content)/song/[id]/edit/page.tsx b/apps/frontend/src/app/(content)/song/[id]/edit/page.tsx
index 8af0f5de..ade9be98 100644
--- a/apps/frontend/src/app/(content)/song/[id]/edit/page.tsx
+++ b/apps/frontend/src/app/(content)/song/[id]/edit/page.tsx
@@ -1,3 +1,4 @@
+
import { redirect } from 'next/navigation';
import { checkLogin } from '@web/modules/auth/features/auth.utils';
diff --git a/apps/frontend/src/app/(content)/song/[id]/page.tsx b/apps/frontend/src/app/(content)/song/[id]/page.tsx
index 198a9c96..a4057eb5 100644
--- a/apps/frontend/src/app/(content)/song/[id]/page.tsx
+++ b/apps/frontend/src/app/(content)/song/[id]/page.tsx
@@ -5,6 +5,7 @@ import { cookies } from 'next/headers';
import axios from '@web/lib/axios';
import { SongPage } from '@web/modules/song/components/SongPage';
+
interface SongPage {
params: {
id: string;
@@ -39,15 +40,15 @@ export async function generateMetadata({
}
return {
- title: song.title,
+ title : song.title,
description: song.description,
- authors: [{ name: song.uploader.username }],
- openGraph: {
- url: publicUrl + '/song/' + song.publicId,
- title: song.title,
+ authors : [{ name: song.uploader.username }],
+ openGraph : {
+ url : publicUrl + '/song/' + song.publicId,
+ title : song.title,
description: song.description,
- siteName: 'Note Block World',
- images: [song.thumbnailUrl],
+ siteName : 'Note Block World',
+ images : [song.thumbnailUrl],
},
twitter: {
card: 'summary_large_image',
diff --git a/apps/frontend/src/app/(content)/upload/page.tsx b/apps/frontend/src/app/(content)/upload/page.tsx
index bccf0c9d..18467ef8 100644
--- a/apps/frontend/src/app/(content)/upload/page.tsx
+++ b/apps/frontend/src/app/(content)/upload/page.tsx
@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
import { checkLogin, getUserData } from '@web/modules/auth/features/auth.utils';
import { UploadSongPage } from '@web/modules/song-upload/components/client/UploadSongPage';
+
export const metadata: Metadata = {
title: 'Upload song',
};
diff --git a/apps/frontend/src/app/(external)/(auth)/login/email/page.tsx b/apps/frontend/src/app/(external)/(auth)/login/email/page.tsx
index be8b80b9..0cd59aee 100644
--- a/apps/frontend/src/app/(external)/(auth)/login/email/page.tsx
+++ b/apps/frontend/src/app/(external)/(auth)/login/email/page.tsx
@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
import { LoginWithEmailPage } from '@web/modules/auth/components/loginWithEmailPage';
import { checkLogin } from '@web/modules/auth/features/auth.utils';
+
export const metadata: Metadata = {
title: 'Sign in',
};
diff --git a/apps/frontend/src/app/(external)/(auth)/login/page.tsx b/apps/frontend/src/app/(external)/(auth)/login/page.tsx
index b8c1de65..e3a3e4e8 100644
--- a/apps/frontend/src/app/(external)/(auth)/login/page.tsx
+++ b/apps/frontend/src/app/(external)/(auth)/login/page.tsx
@@ -4,6 +4,7 @@ import { redirect } from 'next/navigation';
import { LoginPage } from '@web/modules/auth/components/loginPage';
import { checkLogin } from '@web/modules/auth/features/auth.utils';
+
export const metadata: Metadata = {
title: 'Sign in',
};
diff --git a/apps/frontend/src/app/(external)/(auth)/logout/page.tsx b/apps/frontend/src/app/(external)/(auth)/logout/page.tsx
index f69f55f3..d438877a 100644
--- a/apps/frontend/src/app/(external)/(auth)/logout/page.tsx
+++ b/apps/frontend/src/app/(external)/(auth)/logout/page.tsx
@@ -4,6 +4,7 @@ import { toast } from 'react-hot-toast';
import { useSignOut } from '@web/modules/auth/components/client/login.util';
+
function Page() {
useSignOut();
toast.success('You have been logged out!');
diff --git a/apps/frontend/src/app/(external)/(legal)/guidelines/page.tsx b/apps/frontend/src/app/(external)/(legal)/guidelines/page.tsx
index 5ba331fc..c78dabda 100644
--- a/apps/frontend/src/app/(external)/(legal)/guidelines/page.tsx
+++ b/apps/frontend/src/app/(external)/(legal)/guidelines/page.tsx
@@ -5,6 +5,7 @@ import { Metadata } from 'next';
import { CustomMarkdown } from '@web/modules/shared/components/CustomMarkdown';
+
export const metadata: Metadata = {
title: 'Community Guidelines',
};
diff --git a/apps/frontend/src/app/(external)/(legal)/privacy/page.tsx b/apps/frontend/src/app/(external)/(legal)/privacy/page.tsx
index 4ef11c59..82228df3 100644
--- a/apps/frontend/src/app/(external)/(legal)/privacy/page.tsx
+++ b/apps/frontend/src/app/(external)/(legal)/privacy/page.tsx
@@ -5,6 +5,7 @@ import { Metadata } from 'next';
import { CustomMarkdown } from '@web/modules/shared/components/CustomMarkdown';
+
export const metadata: Metadata = {
title: 'Privacy Policy',
};
diff --git a/apps/frontend/src/app/(external)/(legal)/terms/page.tsx b/apps/frontend/src/app/(external)/(legal)/terms/page.tsx
index 40290108..b8d2ed3b 100644
--- a/apps/frontend/src/app/(external)/(legal)/terms/page.tsx
+++ b/apps/frontend/src/app/(external)/(legal)/terms/page.tsx
@@ -5,6 +5,7 @@ import { Metadata } from 'next';
import { CustomMarkdown } from '@web/modules/shared/components/CustomMarkdown';
+
export const metadata: Metadata = {
title: 'Terms of Service',
};
diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx
index 76ddeaff..9b2b3d97 100644
--- a/apps/frontend/src/app/layout.tsx
+++ b/apps/frontend/src/app/layout.tsx
@@ -15,7 +15,7 @@ import { TooltipProvider } from '../modules/shared/components/tooltip';
const lato = Lato({
subsets: ['latin'],
- weight: ['100', '300', '400', '700', '900'],
+ weight : ['100', '300', '400', '700', '900'],
});
export const metadata: Metadata = {
@@ -23,21 +23,21 @@ export const metadata: Metadata = {
description:
'Discover, share and listen to note block music from all around the world',
applicationName: 'Note Block World',
- keywords: ['note block', 'music', 'minecraft', 'nbs', 'note block studio'],
- openGraph: {
- type: 'website',
- images: `${process.env.NEXT_PUBLIC_URL}/nbw-header.png`,
- locale: 'en_US',
- url: process.env.NEXT_PUBLIC_URL,
+ keywords : ['note block', 'music', 'minecraft', 'nbs', 'note block studio'],
+ openGraph : {
+ type : 'website',
+ images : `${process.env.NEXT_PUBLIC_URL}/nbw-header.png`,
+ locale : 'en_US',
+ url : process.env.NEXT_PUBLIC_URL,
siteName: 'Note Block World',
},
};
const jsonLd: WithContext = {
'@context': 'https://schema.org',
- '@type': 'WebSite',
- name: 'Note Block World',
- url: process.env.NEXT_PUBLIC_URL,
+ '@type' : 'WebSite',
+ name : 'Note Block World',
+ url : process.env.NEXT_PUBLIC_URL,
description:
'Discover, share and listen to note block music from all around the world',
};
@@ -100,7 +100,7 @@ export default function RootLayout({
position='bottom-center'
toastOptions={{
className: '!bg-zinc-700 !text-white !max-w-fit',
- duration: 4000,
+ duration : 4000,
}}
/>
),
content: matterResult.content,
};
diff --git a/apps/frontend/src/modules/auth/components/client/LoginFrom.tsx b/apps/frontend/src/modules/auth/components/client/LoginFrom.tsx
index ab4a701c..1b9e89a4 100644
--- a/apps/frontend/src/modules/auth/components/client/LoginFrom.tsx
+++ b/apps/frontend/src/modules/auth/components/client/LoginFrom.tsx
@@ -8,6 +8,7 @@ import { toast } from 'react-hot-toast';
import ClientAxios from '@web/lib/axios/ClientAxios';
+
import {
Input,
SubmitButton,
diff --git a/apps/frontend/src/modules/auth/components/loginPage.tsx b/apps/frontend/src/modules/auth/components/loginPage.tsx
index 5551d6d3..bbf92854 100644
--- a/apps/frontend/src/modules/auth/components/loginPage.tsx
+++ b/apps/frontend/src/modules/auth/components/loginPage.tsx
@@ -8,6 +8,7 @@ import Link from 'next/link';
import { baseApiURL } from '@web/lib/axios';
+
import { CopyrightFooter } from '../../shared/components/layout/CopyrightFooter';
import { NoteBlockWorldLogo } from '../../shared/components/NoteBlockWorldLogo';
diff --git a/apps/frontend/src/modules/auth/components/loginWithEmailPage.tsx b/apps/frontend/src/modules/auth/components/loginWithEmailPage.tsx
index 9ba806f1..06367fd3 100644
--- a/apps/frontend/src/modules/auth/components/loginWithEmailPage.tsx
+++ b/apps/frontend/src/modules/auth/components/loginWithEmailPage.tsx
@@ -1,7 +1,8 @@
-import { LoginForm } from './client/LoginFrom';
import { CopyrightFooter } from '../../shared/components/layout/CopyrightFooter';
import { NoteBlockWorldLogo } from '../../shared/components/NoteBlockWorldLogo';
+import { LoginForm } from './client/LoginFrom';
+
export const LoginWithEmailPage = () => {
return (
{
diff --git a/apps/frontend/src/modules/browse/components/HomePageComponent.tsx b/apps/frontend/src/modules/browse/components/HomePageComponent.tsx
index 4ea9ecb0..e9dd3d19 100644
--- a/apps/frontend/src/modules/browse/components/HomePageComponent.tsx
+++ b/apps/frontend/src/modules/browse/components/HomePageComponent.tsx
@@ -3,13 +3,6 @@
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { CategoryButtonGroup } from './client/CategoryButton';
-import { useFeaturedSongsProvider } from './client/context/FeaturedSongs.context';
-import { useRecentSongsProvider } from './client/context/RecentSongs.context';
-import LoadMoreButton from './client/LoadMoreButton';
-import { TimespanButtonGroup } from './client/TimespanButton';
-import SongCard from './SongCard';
-import SongCardGroup from './SongCardGroup';
import {
InterSectionAdSlot,
SongCardAdSlot,
@@ -24,6 +17,14 @@ import {
} from '../../shared/components/client/Carousel';
import { WelcomeBanner } from '../WelcomeBanner';
+import { CategoryButtonGroup } from './client/CategoryButton';
+import { useFeaturedSongsProvider } from './client/context/FeaturedSongs.context';
+import { useRecentSongsProvider } from './client/context/RecentSongs.context';
+import LoadMoreButton from './client/LoadMoreButton';
+import { TimespanButtonGroup } from './client/TimespanButton';
+import SongCard from './SongCard';
+import SongCardGroup from './SongCardGroup';
+
export const HomePageComponent = () => {
const { featuredSongsPage } = useFeaturedSongsProvider();
@@ -46,8 +47,8 @@ export const HomePageComponent = () => {
diff --git a/apps/frontend/src/modules/browse/components/SongCard.tsx b/apps/frontend/src/modules/browse/components/SongCard.tsx
index d5025023..3883e3e5 100644
--- a/apps/frontend/src/modules/browse/components/SongCard.tsx
+++ b/apps/frontend/src/modules/browse/components/SongCard.tsx
@@ -8,6 +8,7 @@ import Skeleton from 'react-loading-skeleton';
import { formatDuration, formatTimeAgo } from '@web/modules/shared/util/format';
+
import SongThumbnail from '../../shared/components/layout/SongThumbnail';
const SongDataDisplay = ({ song }: { song: SongPreviewDtoType | null }) => {
diff --git a/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx b/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx
index 0de6bb2c..615bdb4c 100644
--- a/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx
+++ b/apps/frontend/src/modules/browse/components/client/CategoryButton.tsx
@@ -28,11 +28,11 @@ export const CategoryButtonGroup = () => {
diff --git a/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx b/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx
index 8b32d8a1..2e1c6fa2 100644
--- a/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx
+++ b/apps/frontend/src/modules/browse/components/client/context/FeaturedSongs.context.tsx
@@ -43,7 +43,6 @@ export function FeaturedSongsProvider({
);
useEffect(() => {
- // eslint-disable-next-line react-hooks/exhaustive-deps
setFeaturedSongsPage(featuredSongs[timespan]);
}, [featuredSongs, timespan]);
diff --git a/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx b/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx
index 8d7fe9b7..f02d1130 100644
--- a/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx
+++ b/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx
@@ -1,16 +1,11 @@
'use client';
import { SongPreviewDtoType } from '@nbw/database';
-import {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useState,
-} from 'react';
+import { createContext, useCallback, useContext, useEffect, useState, } from 'react';
import axiosInstance from '@web/lib/axios';
+
type RecentSongsContextType = {
recentSongs: (SongPreviewDtoType | null)[];
recentError: string;
@@ -43,7 +38,7 @@ export function RecentSongsProvider({
const [hasMore, setHasMore] = useState(true);
const [categories, setCategories] = useState>({});
const [selectedCategory, setSelectedCategory] = useState('');
- const [endpoint, setEndpoint] = useState('/song-browser/recent');
+ const [endpoint, setEndpoint] = useState('/song?q=recent');
const adCount = 1;
const pageSize = 12;
@@ -97,7 +92,7 @@ export function RecentSongsProvider({
const fetchCategories = useCallback(async function () {
try {
const response = await axiosInstance.get>(
- '/song-browser/categories',
+ '/songs?q=categories',
);
return response.data;
@@ -119,8 +114,8 @@ export function RecentSongsProvider({
const newEndpoint =
selectedCategory === ''
- ? '/song-browser/recent'
- : `/song-browser/categories/${selectedCategory}`;
+ ? '/song?q=recent'
+ : `/songs?q=categories&id=${selectedCategory}`;
setEndpoint(newEndpoint);
}, [selectedCategory]);
diff --git a/apps/frontend/src/modules/my-songs/components/MySongsPage.tsx b/apps/frontend/src/modules/my-songs/components/MySongsPage.tsx
index 93ee1b15..3e62542c 100644
--- a/apps/frontend/src/modules/my-songs/components/MySongsPage.tsx
+++ b/apps/frontend/src/modules/my-songs/components/MySongsPage.tsx
@@ -3,9 +3,10 @@ import type { SongPageDtoType, SongsFolder } from '@nbw/database';
import axiosInstance from '@web/lib/axios';
+import { getTokenServer } from '../../auth/features/auth.utils';
+
import { MySongProvider } from './client/context/MySongs.context';
import { MySongsPageComponent } from './client/MySongsTable';
-import { getTokenServer } from '../../auth/features/auth.utils';
async function fetchSongsPage(
page: number,
@@ -18,9 +19,9 @@ async function fetchSongsPage(
authorization: `Bearer ${token}`,
},
params: {
- page: page + 1,
+ page : page + 1,
limit: pageSize,
- sort: 'createdAt',
+ sort : 'createdAt',
order: false,
},
})
@@ -62,9 +63,9 @@ async function fetchSongsFolder(): Promise {
return {
0: {
content: [],
- total: 0,
- page: 0,
- limit: 0,
+ total : 0,
+ page : 0,
+ limit : 0,
},
};
}
diff --git a/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx b/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx
index 2140824d..dd76e6ee 100644
--- a/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx
+++ b/apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx
@@ -12,6 +12,7 @@ import Skeleton from 'react-loading-skeleton';
import { ErrorBox } from '@web/modules/shared/components/client/ErrorBox';
+
import { useMySongsProvider } from './context/MySongs.context';
import DeleteConfirmDialog from './DeleteConfirmDialog';
import { SongRow } from './SongRow';
diff --git a/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx b/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx
index 7bc60297..da97a5a9 100644
--- a/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx
+++ b/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx
@@ -12,13 +12,15 @@ import Skeleton from 'react-loading-skeleton';
import SongThumbnail from '@web/modules/shared/components/layout/SongThumbnail';
import { formatDuration } from '@web/modules/shared/util/format';
-import { useMySongsProvider } from './context/MySongs.context';
+
import {
DeleteButton,
DownloadSongButton,
EditButton,
} from '../client/MySongsButtons';
+import { useMySongsProvider } from './context/MySongs.context';
+
export const SongRow = ({ song }: { song?: SongPreviewDtoType | null }) => {
const { setIsDeleteDialogOpen, setSongToDelete } = useMySongsProvider();
@@ -123,9 +125,9 @@ export const SongRow = ({ song }: { song?: SongPreviewDtoType | null }) => {
) : (
<>
{new Date(song.createdAt).toLocaleDateString('en-US', {
- day: 'numeric',
+ day : 'numeric',
month: 'short',
- year: 'numeric',
+ year : 'numeric',
})}
>
)}
diff --git a/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx b/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx
index 72737afd..ccde434f 100644
--- a/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx
+++ b/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx
@@ -18,6 +18,7 @@ import { toast } from 'react-hot-toast';
import axiosInstance from '@web/lib/axios';
import { getTokenLocal } from '@web/lib/axios/token.utils';
+
type MySongsContextType = {
page: SongPageDtoType | null;
nextpage: () => void;
@@ -86,9 +87,9 @@ export const MySongProvider = ({
try {
const response = await axiosInstance.get('/my-songs', {
params: {
- page: currentPage,
+ page : currentPage,
limit: pageSize,
- sort: 'createdAt',
+ sort : 'createdAt',
order: 'false',
},
headers: {
@@ -100,7 +101,7 @@ export const MySongProvider = ({
// TODO: total, page and pageSize are stored in every page, when it should be stored in the folder (what matters is 'content')
putPage({
- key: currentPage,
+ key : currentPage,
page: data,
});
diff --git a/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx b/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx
index 83c97fd2..c2dcfc31 100644
--- a/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx
+++ b/apps/frontend/src/modules/shared/components/NoteBlockWorldLogo.tsx
@@ -2,6 +2,7 @@ import Image from 'next/image';
import { cn } from '@web/lib/tailwind.utils';
+
export const NoteBlockWorldLogo = ({
size,
orientation = 'adaptive',
diff --git a/apps/frontend/src/modules/shared/components/client/FormElements.tsx b/apps/frontend/src/modules/shared/components/client/FormElements.tsx
index 5d453377..c107be1b 100644
--- a/apps/frontend/src/modules/shared/components/client/FormElements.tsx
+++ b/apps/frontend/src/modules/shared/components/client/FormElements.tsx
@@ -8,6 +8,7 @@ import Markdown from 'react-markdown';
import { cn } from '@web/lib/tailwind.utils';
import { ErrorBalloon } from '@web/modules/shared/components/client/ErrorBalloon';
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
export const Label = forwardRef<
diff --git a/apps/frontend/src/modules/shared/components/client/UserMenuButton.tsx b/apps/frontend/src/modules/shared/components/client/UserMenuButton.tsx
index 23adda59..23ed88b1 100644
--- a/apps/frontend/src/modules/shared/components/client/UserMenuButton.tsx
+++ b/apps/frontend/src/modules/shared/components/client/UserMenuButton.tsx
@@ -4,6 +4,7 @@ import Image from 'next/image';
import { LoggedUserData } from '@web/modules/auth/types/User';
+
export function UserMenuButton({ userData }: { userData: LoggedUserData }) {
return (
<>
diff --git a/apps/frontend/src/modules/shared/components/client/ads/AdSlots.tsx b/apps/frontend/src/modules/shared/components/client/ads/AdSlots.tsx
index 9128fba9..72d68608 100644
--- a/apps/frontend/src/modules/shared/components/client/ads/AdSlots.tsx
+++ b/apps/frontend/src/modules/shared/components/client/ads/AdSlots.tsx
@@ -7,6 +7,7 @@ import { useEffect, useState } from 'react';
import { cn } from '@web/lib/tailwind.utils';
+
import useAdSenseClient from './useAdSenseClient';
const HideAdButton = ({
@@ -18,10 +19,8 @@ const HideAdButton = ({