Skip to content

Commit

Permalink
Add pagination and make replay cards look more funky
Browse files Browse the repository at this point in the history
  • Loading branch information
Simyon264 committed Mar 23, 2024
1 parent de2b367 commit ac89039
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .idea/.idea.ReplayBrowser/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions Client/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
</div>

<script>
// This is very cursed, but it works
// I move all modals to the modal-root div
// also every time a modal is created, it is appended to the modal-root div
const modalRoot = document.getElementById('modal-root');
const pageRoot = document.querySelector('.page');
Expand All @@ -27,7 +24,6 @@
modalRoot.appendChild(modal);
});
// listen for when a div is created with the class modal and move it to the modal-root div
const observer = new MutationObserver(function(mutations) {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
Expand Down
45 changes: 41 additions & 4 deletions Client/Components/Pages/Search.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@using Replay = Shared.Models.Replay
@using System.Net
@using System.Diagnostics
@using Shared
@inject HttpClient Http
@inject NavigationManager NavigationManager

Expand Down Expand Up @@ -41,24 +42,57 @@
break;
default:
{
<p>Found @Replays.Count replays in @stopWatch.ElapsedMilliseconds ms</p>
<p>Found @TotalReplays replays in @stopWatch.ElapsedMilliseconds ms</p>
<p>Page @Page of @TotalPages</p>
<div class="replay-list">
@foreach (var replay in Replays)
{
<Replay ReplayData="replay"></Replay>
}
</div>

<br/>
<div class="pagination">
@if (Page > 1)
{
<button class="btn btn-primary" onclick="pageDown()">Previous page</button>
} else
{
<button class="btn btn-primary" disabled>Previous page</button>
}
@if (Page < TotalPages)
{
<button class="btn btn-primary" onclick="pageUp()">Next page</button>
} else
{
<button class="btn btn-primary" disabled>Next page</button>
}
</div>
break;
}
}

<script>
const currentPage = @Page;
const totalPages = @TotalPages;
function pageUp() {
if (currentPage == totalPages) return;
search(currentPage + 1);
}
function pageDown() {
if (currentPage == 1) return;
search(currentPage - 1);
}
</script>

@code {
public List<Replay> Replays { get; set; } = new List<Replay>();
public bool IsLoading { get; set; } = true;
public string? ErrorMessage { get; set; }
public string? ErrorDetails { get; set; }
public Stopwatch stopWatch { get; set; } = new Stopwatch();
public int Page { get; set; } = 1;
public int TotalPages { get; set; } = 1;
public int TotalReplays { get; set; } = 0;

protected override async Task OnInitializedAsync()
{
Expand Down Expand Up @@ -87,7 +121,7 @@
return;
}

var loadedReplays = await response.Content.ReadFromJsonAsync<List<Replay>>();
var loadedReplays = await response.Content.ReadFromJsonAsync<SearchResult>();
if (loadedReplays == null)
{
ErrorMessage = "Failed to load replays";
Expand All @@ -96,7 +130,10 @@
}

stopWatch.Stop();
Replays = loadedReplays;
Replays = loadedReplays.Replays;
Page = loadedReplays.CurrentPage;
TotalPages = loadedReplays.PageCount;
TotalReplays = loadedReplays.TotalReplays;
IsLoading = false;
}
}
10 changes: 10 additions & 0 deletions Client/Components/Pages/Search.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,14 @@
flex-wrap: wrap;
justify-content: space-between;
margin: 0 0 1rem 0;
}

.pagination {
display: flex;
justify-content: center;
margin: 1rem 0;
}

.pagination .btn {
margin: 0 0.5rem;
}
2 changes: 2 additions & 0 deletions Client/Components/Replay.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<h5 class="card-title">@_nameFormatted</h5>
<p class="card-text">Map: @ReplayData.Map</p>
<p class="card-text">Gamemode: @ReplayData.Gamemode</p>
</div>
<div class="card-footer">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#r-@ReplayData.Id">
View replay
</button>
Expand Down
5 changes: 3 additions & 2 deletions Client/Components/SearchBar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,20 @@
});
});
function search() {
function search(page) {
const searchMode = document.getElementById('searchModeButton').textContent;
const searchText = document.querySelector('.search-bar input').value;
const builder = new URLSearchParams();
builder.append('mode', searchMode);
builder.append('query', searchText);
builder.append('page', page);
window.location.href = '/search?' + builder.toString();
}
document.querySelector('.search-bar input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
search();
search(1);
}
});
Expand Down
26 changes: 24 additions & 2 deletions Server/Api/ReplayController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.EntityFrameworkCore;
using Shared;
using System.Collections.Generic;
using Server.Helpers;

namespace Server.Api;

Expand Down Expand Up @@ -45,7 +46,8 @@ public async Task<ActionResult> UploadReplay(IFormFile file)
[Route("/search")]
public async Task<ActionResult> SearchReplays(

Check warning on line 47 in Server/Api/ReplayController.cs

View workflow job for this annotation

GitHub Actions / deploy

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 47 in Server/Api/ReplayController.cs

View workflow job for this annotation

GitHub Actions / deploy

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
[FromQuery] string mode,
[FromQuery] string query
[FromQuery] string query,
[FromQuery] int page = 0
)
{
var searchMode = SearchMode.Gamemode;
Expand Down Expand Up @@ -83,12 +85,32 @@ [FromQuery] string query
{
searchMode = modeEnum;
}

if (string.IsNullOrWhiteSpace(query))
{
return BadRequest("The search query cannot be empty.");
}

if (page < 0)
{
return BadRequest("The page number cannot be negative.");
}

var found = ReplayParser.SearchReplays(searchMode, query, _context);
// Order found replays by date
found = found.OrderByDescending(r => r.Date ?? DateTime.MinValue).Take(Constants.SearchLimit).ToList();

return Ok(found);
var pageCount = Paginator.GetPageCount(found.Count, Constants.ReplaysPerPage);
var offset = Paginator.GetOffset(page, Constants.ReplaysPerPage);
var paginated = found.Skip(offset).Take(Constants.ReplaysPerPage).ToList();

return Ok(new SearchResult()
{
Replays = paginated,
PageCount = pageCount,
CurrentPage = page,
TotalReplays = found.Count
});
}

[HttpGet]
Expand Down
31 changes: 31 additions & 0 deletions Server/Helpers/Paginator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Server.Helpers;

/// <summary>
/// A class that helps with pagination.
/// </summary>
public class Paginator
{
/// <summary>
/// Returns the number of pages needed to display all items.
/// </summary>
public static int GetPageCount(int totalItems, int itemsPerPage)
{
return (int) Math.Ceiling((double) totalItems / itemsPerPage);
}

/// <summary>
/// Returns the offset for a given page.
/// </summary>
public static int GetOffset(int page, int itemsPerPage)
{
return page * itemsPerPage;
}

/// <summary>
/// Returns the page for a given offset.
/// </summary>
public static int GetPage(int offset, int itemsPerPage)
{
return offset / itemsPerPage;
}
}
1 change: 1 addition & 0 deletions Shared/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
public class Constants
{
public const int SearchLimit = int.MaxValue;
public const int ReplaysPerPage = 32;
}
11 changes: 11 additions & 0 deletions Shared/SearchResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Shared.Models;

namespace Shared;

public class SearchResult
{
public int PageCount { get; set; }
public int CurrentPage { get; set; }
public List<Replay> Replays { get; set; }

Check warning on line 9 in Shared/SearchResult.cs

View workflow job for this annotation

GitHub Actions / deploy

Non-nullable property 'Replays' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public int TotalReplays { get; set; }
}

0 comments on commit ac89039

Please sign in to comment.