Skip to content

Commit

Permalink
Merge pull request #404 from Godsmiracle001/pagination-feature
Browse files Browse the repository at this point in the history
Implemented pagination logic (issue #398)
  • Loading branch information
PeterOche authored Mar 4, 2025
2 parents 77f8f89 + c5dc17f commit 969ac70
Show file tree
Hide file tree
Showing 18 changed files with 628 additions and 315 deletions.
17 changes: 3 additions & 14 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module, } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
Expand Down Expand Up @@ -26,25 +26,17 @@ import { TournamentModule } from './tournament/tournament.module';
import { GameGateway } from './websocket-game comms/providers/gamegateway';
import { GameModule } from './websocket-game comms/game.module';
import { AchievementModule } from './achievement/achievement.module';

import { MusicTheoryLessonModule } from './music-education/music-theory-lesson.module';

import { GameModeModule } from './game-mode/game-mode.module';

import { SongGenreModule } from './song-genre/song-genre.module';

import { SocialModule } from './social/social.module';
// import { AchievementModule } from './achievement/achievement.module';
import { CacheModule } from '@nestjs/cache-manager';
import * as redisStore from 'cache-manager-redis-store';
import { ThrottlerModule } from '@nestjs/throttler';
<<<<<<< HEAD
import { ReferralModule } from './referral/referral.module';
=======
import { GameInsightsModule } from './game-insights/game-insights.module';
import { PaginationModule } from './common/pagination/pagination.module';
import { StateRecoveryModule } from './state-recovery/state-recovery.module';
>>>>>>> 1254719ee782ca271ea231ebe706912a91061959

@Module({
imports: [
Expand Down Expand Up @@ -79,7 +71,7 @@ import { StateRecoveryModule } from './state-recovery/state-recovery.module';
host: process.env.REDIS_HOST || 'localhost',
port: Number(process.env.REDIS_PORT) || 6379,
},
ttl: 3600,
ttl: 3600,
}),
SongsModule,
ChatRoomModule,
Expand All @@ -91,12 +83,9 @@ import { StateRecoveryModule } from './state-recovery/state-recovery.module';
MusicTheoryLessonModule,
GameModeModule,
SongGenreModule,
<<<<<<< HEAD
ReferralModule,
StateRecoveryModule,
=======
StateRecoveryModule,
GameInsightsModule,
>>>>>>> 1254719ee782ca271ea231ebe706912a91061959
],
controllers: [AppController],
providers: [
Expand Down
14 changes: 14 additions & 0 deletions backend/src/common/pagination/pagination-query-dto.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IsOptional, IsPositive } from 'class-validator';

import { Type } from 'class-transformer';

export class PaginationQueryDto {
// @Type(() => Number) //coverting strings to numbers
@IsOptional()
@IsPositive()
limit?: number = 10;

@IsOptional()
@IsPositive()
page?: number = 1;
}
18 changes: 9 additions & 9 deletions backend/src/common/pagination/provider/pagination.provider.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Injectable, Inject } from "@nestjs/common";
import { PaginationQueryDto } from "src/common/nterceptors/data-response/pagination/pagination-query.dto";
import { ObjectLiteral, Repository } from "typeorm";
import { Request } from "express";
import { REQUEST } from "@nestjs/core";
import { paginated } from "../interfaces/pagination-interface";
import { Injectable, Inject } from '@nestjs/common';
import { ObjectLiteral, Repository } from 'typeorm';
import { Request } from 'express';
import { REQUEST } from '@nestjs/core';
import { paginated } from '../interfaces/pagination-interface';
import { PaginationQueryDto } from '../pagination-query-dto.dto';

@Injectable()
export class PaginationProvider {
constructor(
@Inject(REQUEST)
private readonly request: Request
private readonly request: Request,
) {}
public async paginationQuery<T extends ObjectLiteral>(
//type generic is a type that is used when we are unsure of the type of object to return
paginatedQueryDto: PaginationQueryDto,
repository: Repository<T>
repository: Repository<T>,
): Promise<paginated<T>> {
const result = await repository.find({
skip: paginatedQueryDto.limit * (paginatedQueryDto.page - 1),
Expand All @@ -24,7 +24,7 @@ export class PaginationProvider {
//create a request url
//create a variable called base url
const baseUrl =
this.request.protocol + "://" + this.request.headers.host + "/";
this.request.protocol + '://' + this.request.headers.host + '/';
console.log(baseUrl);

const newUrl = new URL(this.request.url, baseUrl);
Expand Down
19 changes: 19 additions & 0 deletions backend/src/game-results/dto/get-gamesresult-base-dto.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsDate, IsOptional } from 'class-validator';
import { IntersectionType } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { PaginationQueryDto } from 'src/common/pagination/pagination-query-dto.dto';

class GetGamesResultBaseDto {
@IsDate()
@IsOptional()
startDate?: Date;

@IsDate()
@IsOptional()
endDate?: Date;
}

export class GetGamesResultsDto extends IntersectionType(
GetGamesResultBaseDto,
PaginationQueryDto,
) {}
34 changes: 27 additions & 7 deletions backend/src/game-results/game-results.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { Controller, Get, Post, Body, Param, Query, UseGuards } from '@nestjs/common';
import {
Controller,
Get,
Post,
Body,
Param,
Query,
UseGuards,
DefaultValuePipe,
ParseIntPipe,
} from '@nestjs/common';
import { GameResultsService } from './game-results.service';
import { CreateGameResultDto } from './dto/game-result.dto';
import { GameResult } from './entities/game-result.entity';
import { LeaderboardEntryDto } from './dto/leaderboard-entry.dto';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
// Assuming you have some form of authentication
// import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';

Expand All @@ -12,7 +23,9 @@ export class GameResultsController {

@Post()
// @UseGuards(JwtAuthGuard)
async createResult(@Body() createGameResultDto: CreateGameResultDto): Promise<GameResult> {
async createResult(
@Body() createGameResultDto: CreateGameResultDto,
): Promise<GameResult> {
return this.gameResultsService.createResult(createGameResultDto);
}

Expand All @@ -33,10 +46,17 @@ export class GameResultsController {
return this.gameResultsService.getUserBest(userId, gameId);
}

@Get('user/:userId')
// @UseGuards(JwtAuthGuard)
async getUserResults(@Param('userId') userId: string): Promise<GameResult[]> {
return this.gameResultsService.getUserResults(userId);
@Get('/user/:userId')
@ApiOperation({ summary: 'Get user results with pagination' })
@ApiResponse({
status: 200,
description: 'List of user results successfully retrieved',
})
public getUserResults(
@Param('userId') userId: string,
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
): Promise<GameResult[]> {
return this.gameResultsService.getUserResults(userId, page, limit);
}
}

51 changes: 34 additions & 17 deletions backend/src/game-results/game-results.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,39 @@ export class GameResultsService {
// Implement your scoring algorithm based on your game's logic
// Example: Base score from game + time bonus + achievement bonus
let finalScore = gameData.baseScore || 0;

// Time bonus (example: faster completion = higher bonus)
const timeBonus = Math.max(1000 - timeSpent, 0) * 0.1;
finalScore += timeBonus;

// Achievement bonus
if (gameData.achievements && Array.isArray(gameData.achievements)) {
const achievementBonus = gameData.achievements.length * 50;
finalScore += achievementBonus;
}

// Round to integer
return Math.round(finalScore);
}

/**
* Create and store a new game result
*/
async createResult(createGameResultDto: CreateGameResultDto): Promise<GameResult> {
async createResult(
createGameResultDto: CreateGameResultDto,
): Promise<GameResult> {
// Calculate final score if not provided
if (!createGameResultDto.score) {
createGameResultDto.score = this.calculateFinalScore(
createGameResultDto.gameData,
createGameResultDto.timeSpent,
);
}

// Create and save the entity
const gameResult = this.gameResultsRepository.create(createGameResultDto);
const savedResult = await this.gameResultsRepository.save(gameResult);

// Emit event for other parts of the system
this.eventEmitter.emit(
'game.result.created',
Expand All @@ -65,30 +67,35 @@ export class GameResultsService {
savedResult.achievements || [],
),
);

this.logger.log(`Game result created for user ${savedResult.userId} with score ${savedResult.score}`);


this.logger.log(
`Game result created for user ${savedResult.userId} with score ${savedResult.score}`,
);

return savedResult;
}

/**
* Generate a leaderboard for a specific game
*/
async generateLeaderboard(gameId: string, limit = 10): Promise<LeaderboardEntryDto[]> {
async generateLeaderboard(
gameId: string,
limit = 10,
): Promise<LeaderboardEntryDto[]> {
// Get top scores for the specified game
const results = await this.gameResultsRepository.find({
where: { gameId },
order: { score: 'DESC' },
take: limit,
});

// Transform into leaderboard entries with ranks
const leaderboard = await Promise.all(
results.map(async (result, index) => {
// In a real app, you might want to fetch username from a user service
// This is a simplified example
const username = `User_${result.userId.substring(0, 6)}`;

const entry: LeaderboardEntryDto = {
userId: result.userId,
username,
Expand All @@ -97,18 +104,21 @@ export class GameResultsService {
achievements: result.achievements || [],
gameId: result.gameId,
};

return entry;
}),
);

return leaderboard;
}

/**
* Get user's personal best for a specific game
*/
async getUserBest(userId: string, gameId: string): Promise<GameResult | null> {
async getUserBest(
userId: string,
gameId: string,
): Promise<GameResult | null> {
return this.gameResultsRepository.findOne({
where: { userId, gameId },
order: { score: 'DESC' },
Expand All @@ -118,10 +128,17 @@ export class GameResultsService {
/**
* Get all results for a specific user
*/
async getUserResults(userId: string): Promise<GameResult[]> {
async getUserResults(
userId: string,
page: number = 1,
limit: number = 20,
): Promise<GameResult[]> {
const offset = (page - 1) * limit;
return this.gameResultsRepository.find({
where: { userId },
order: { createdAt: 'DESC' },
skip: offset,
take: limit,
});
}
}
}
20 changes: 20 additions & 0 deletions backend/src/questions/dto/get-question-base-dto.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IsDate, IsOptional } from 'class-validator';

import { IntersectionType } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { PaginationQueryDto } from 'src/common/pagination/pagination-query-dto.dto';

class GetQuestionBaseDto {
@IsDate()
@IsOptional()
startDate?: Date;

@IsDate()
@IsOptional()
endDate?: Date;
}

export class GetQuestionDto extends IntersectionType(
GetQuestionBaseDto,
PaginationQueryDto,
) {}
Loading

0 comments on commit 969ac70

Please sign in to comment.