@@ -56,17 +56,25 @@
}
-@if(_consoles?.Count > 0)
+@if(_total > 0)
{
-
@string.Format(L["{0} videogame consoles found in the database."], _consoles.Count)
-
- @foreach(MachineDto console in _consoles)
+ @string.Format(L["{0} videogame consoles found in the database."], _total)
+
}
else
{
@@ -78,4 +86,4 @@ else
{
@L["There are no videogame consoles found introduced this year."]
}
-}
\ No newline at end of file
+}
diff --git a/Marechai/Pages/Consoles/Search.razor.cs b/Marechai/Pages/Consoles/Search.razor.cs
index bfd0a6d1..e09ca448 100644
--- a/Marechai/Pages/Consoles/Search.razor.cs
+++ b/Marechai/Pages/Consoles/Search.razor.cs
@@ -23,21 +23,33 @@
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Marechai.ApiClient.Models;
using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
namespace Marechai.Pages.Consoles;
-public partial class Search
+public partial class Search : IAsyncDisposable
{
- char? _character;
- List _consoles;
- bool _loaded;
- bool _showPrototypes;
- string _lastStartingCharacter;
- int? _lastYear;
+ const int _pageSize = 100;
+ readonly string _sentinelId = $"consoles-search-sentinel-{Guid.NewGuid():N}";
+ readonly string _scrollerId = $"consoles-search-scroll-{Guid.NewGuid():N}";
+ readonly List _consoles = [];
+ char? _character;
+ bool _initialized;
+ bool _loadingMore;
+ bool _observerRegistered;
+ bool _showPrototypes;
+ string _lastStartingCharacter;
+ int? _lastYear;
+ int _total;
+ DotNetObjectReference _selfRef;
+
+ [Inject]
+ IJSRuntime JS { get; set; }
[Parameter]
public int? Year { get; set; }
@@ -45,30 +57,29 @@ public partial class Search
[Parameter]
public string StartingCharacter { get; set; }
+ bool HasMore => _consoles.Count < _total;
+
protected override void OnParametersSet()
{
if(Year == _lastYear && StartingCharacter == _lastStartingCharacter) return;
_lastYear = Year;
_lastStartingCharacter = StartingCharacter;
- _loaded = false;
+ _initialized = false;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
- if(_loaded) return;
-
- _character = null;
- _showPrototypes = false;
-
- if(StartingCharacter == "prototypes")
+ if(!_initialized)
{
- _showPrototypes = true;
- _consoles = await Service.GetPrototypesAsync();
- }
- else
- {
- if(!string.IsNullOrWhiteSpace(StartingCharacter) && StartingCharacter.Length == 1)
+ _character = null;
+ _showPrototypes = false;
+ _total = 0;
+ _consoles.Clear();
+
+ if(StartingCharacter == "prototypes")
+ _showPrototypes = true;
+ else if(!string.IsNullOrWhiteSpace(StartingCharacter) && StartingCharacter.Length == 1)
{
_character = StartingCharacter[0];
@@ -79,14 +90,117 @@ public partial class Search
if(_character < '0' || _character > '9' && _character < 'A' || _character > 'Z') _character = null;
}
- if(_character.HasValue) _consoles = await Service.GetConsolesByLetterAsync(_character.Value);
+ // Stop the previous observer (if any) before swapping in a new
+ // batch — the sentinel/scroller IDs are stable per page instance,
+ // but a route-parameter change forces a fresh data load and the
+ // observer must be re-armed once the new sentinel has rendered.
+ await UnobserveAsync();
- if(Year.HasValue && _consoles is null) _consoles = await Service.GetConsolesByYearAsync(Year.Value);
+ // Fetch the total count + first page in parallel so the page is
+ // fully painted in one round-trip pair.
+ Task countTask = GetCurrentCountAsync();
+ Task> firstPageTask = FetchBatchAsync(0, _pageSize);
- _consoles ??= await Service.GetConsolesAsync();
+ await Task.WhenAll(countTask, firstPageTask);
+
+ _total = countTask.Result;
+ _consoles.AddRange(firstPageTask.Result);
+ _initialized = true;
+
+ StateHasChanged();
+ return;
}
- _loaded = true;
- StateHasChanged();
+ // Register the IntersectionObserver once the sentinel has been
+ // rendered. The observer keeps the same target reference, so this is
+ // a one-shot until UnobserveAsync clears the flag.
+ if(!_observerRegistered && HasMore)
+ {
+ _selfRef ??= DotNetObjectReference.Create(this);
+
+ try
+ {
+ await JS.InvokeVoidAsync("marechaiInfiniteScroll.observe", _sentinelId, _selfRef, $"#{_scrollerId}",
+ "200px");
+ _observerRegistered = true;
+ }
+ catch(JSDisconnectedException)
+ {
+ // Circuit gone — nothing to do.
+ }
+ }
}
-}
\ No newline at end of file
+
+ ///
+ /// Invoked by the IntersectionObserver in infinite-scroll.js when the
+ /// sentinel scrolls into view. Loads the next page, appends it to the
+ /// in-memory list, and re-renders.
+ ///
+ [JSInvokable]
+ public async Task OnSentinelVisibleAsync()
+ {
+ if(_loadingMore || !HasMore) return;
+
+ _loadingMore = true;
+ StateHasChanged();
+
+ try
+ {
+ List next = await FetchBatchAsync(_consoles.Count, _pageSize);
+ _consoles.AddRange(next);
+
+ // If the very last page just arrived, stop the observer so it
+ // doesn't keep firing as the user scrolls past the new tail.
+ if(!HasMore) await UnobserveAsync();
+ }
+ finally
+ {
+ _loadingMore = false;
+ StateHasChanged();
+ }
+ }
+
+ Task GetCurrentCountAsync()
+ {
+ if(_showPrototypes) return Service.GetPrototypesCountAsync();
+
+ if(_character.HasValue) return Service.GetConsolesByLetterCountAsync(_character.Value);
+
+ if(Year.HasValue) return Service.GetConsolesByYearCountAsync(Year.Value);
+
+ return Service.GetConsolesCountAsync();
+ }
+
+ Task> FetchBatchAsync(int skip, int take)
+ {
+ if(_showPrototypes) return Service.GetPrototypesAsync(skip, take);
+
+ if(_character.HasValue) return Service.GetConsolesByLetterAsync(_character.Value, skip, take);
+
+ if(Year.HasValue) return Service.GetConsolesByYearAsync(Year.Value, skip, take);
+
+ return Service.GetConsolesAsync(skip, take);
+ }
+
+ async Task UnobserveAsync()
+ {
+ if(!_observerRegistered) return;
+
+ try
+ {
+ await JS.InvokeVoidAsync("marechaiInfiniteScroll.unobserve", _sentinelId);
+ }
+ catch(JSDisconnectedException)
+ {
+ // Circuit gone; nothing to do.
+ }
+
+ _observerRegistered = false;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await UnobserveAsync();
+ _selfRef?.Dispose();
+ }
+}
diff --git a/Marechai/Services/ConsolesService.cs b/Marechai/Services/ConsolesService.cs
index ffbc32c2..d8ad6780 100644
--- a/Marechai/Services/ConsolesService.cs
+++ b/Marechai/Services/ConsolesService.cs
@@ -25,6 +25,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using Marechai.ApiClient.Models;
@@ -74,11 +75,16 @@ public class ConsolesService(Marechai.ApiClient.Client client)
}
}
- public async Task> GetConsolesByLetterAsync(char c)
+ public async Task> GetConsolesByLetterAsync(char c, int? skip = null, int? take = null,
+ CancellationToken cancellationToken = default)
{
try
{
- List machines = await client.Consoles.ByLetter[c.ToString()].GetAsync();
+ List machines = await client.Consoles.ByLetter[c.ToString()].GetAsync(config =>
+ {
+ if(skip.HasValue) config.QueryParameters.Skip = skip.Value;
+ if(take.HasValue) config.QueryParameters.Take = take.Value;
+ }, cancellationToken);
return machines ?? [];
}
@@ -88,11 +94,31 @@ public class ConsolesService(Marechai.ApiClient.Client client)
}
}
- public async Task> GetConsolesByYearAsync(int year)
+ public async Task GetConsolesByLetterCountAsync(char c, CancellationToken cancellationToken = default)
{
try
{
- List machines = await client.Consoles.ByYear[year].GetAsync();
+ int? count = await client.Consoles.ByLetter[c.ToString()].Count
+ .GetAsync(cancellationToken: cancellationToken);
+
+ return count ?? 0;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ public async Task> GetConsolesByYearAsync(int year, int? skip = null, int? take = null,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ List machines = await client.Consoles.ByYear[year].GetAsync(config =>
+ {
+ if(skip.HasValue) config.QueryParameters.Skip = skip.Value;
+ if(take.HasValue) config.QueryParameters.Take = take.Value;
+ }, cancellationToken);
return machines ?? [];
}
@@ -102,11 +128,30 @@ public class ConsolesService(Marechai.ApiClient.Client client)
}
}
- public async Task> GetConsolesAsync()
+ public async Task GetConsolesByYearCountAsync(int year, CancellationToken cancellationToken = default)
{
try
{
- List machines = await client.Consoles.GetAsync();
+ int? count = await client.Consoles.ByYear[year].Count.GetAsync(cancellationToken: cancellationToken);
+
+ return count ?? 0;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ public async Task> GetConsolesAsync(int? skip = null, int? take = null,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ List machines = await client.Consoles.GetAsync(config =>
+ {
+ if(skip.HasValue) config.QueryParameters.Skip = skip.Value;
+ if(take.HasValue) config.QueryParameters.Take = take.Value;
+ }, cancellationToken);
return machines ?? [];
}
@@ -116,11 +161,16 @@ public class ConsolesService(Marechai.ApiClient.Client client)
}
}
- public async Task> GetPrototypesAsync()
+ public async Task> GetPrototypesAsync(int? skip = null, int? take = null,
+ CancellationToken cancellationToken = default)
{
try
{
- List machines = await client.Consoles.Prototypes.GetAsync();
+ List machines = await client.Consoles.Prototypes.GetAsync(config =>
+ {
+ if(skip.HasValue) config.QueryParameters.Skip = skip.Value;
+ if(take.HasValue) config.QueryParameters.Take = take.Value;
+ }, cancellationToken);
return machines ?? [];
}
@@ -130,6 +180,20 @@ public class ConsolesService(Marechai.ApiClient.Client client)
}
}
+ public async Task GetPrototypesCountAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ int? count = await client.Consoles.Prototypes.Count.GetAsync(cancellationToken: cancellationToken);
+
+ return count ?? 0;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
public async Task> GetCompaniesAsync()
{
try