checkpoint

This commit is contained in:
2026-01-13 20:36:03 -06:00
parent d9f48244aa
commit a4b08f4cf1
8 changed files with 250 additions and 46 deletions

View File

@@ -3,9 +3,9 @@
@inject NavigationManager navigator
<main class="PrimaryTheme">
<h1>Login</h1>
<section class="LoginForm">
<form class="LoginForm" @onsubmit="HandleSubmit">
<h1 style="grid-area: h1">Login</h1>
<AuthorizeView>
<Authorized>
<div>You're logged in as @context.User.Identity?.Name.</div>
@@ -13,26 +13,32 @@
<NotAuthorized>
@if (errorList.Length > 0)
{
<ul class="Errors" style="grid-area: errors">
@foreach (var error in errorList)
{
<li>@error</li>
}
</ul>
<div class="Errors" style="grid-area: errors">
<ul>
@foreach (var error in errorList)
{
<li>@error</li>
}
</ul>
<div class="ErrorProgress" @key="errorKey"></div>
</div>
}
<label for="email" style="grid-area: emailLabel">Email</label>
<input required id="email" name="emailInput" type="email" style="grid-area: emailControl" @bind-value="email" />
<input required id="email" name="emailInput" type="email" style="grid-area: emailControl" @bind-value="email" readonly="@isEmailSubmitted" />
<label for="password" style="grid-area: passLabel">Password</label>
<input required id="password" name="passwordInput" type="password" style="grid-area: passControl" @bind-value="password" />
@if (isEmailSubmitted)
{
<label for="password" style="grid-area: passLabel">Password</label>
<input required id="password" name="passwordInput" type="password" style="grid-area: passControl" @bind-value="password" />
<a href="forgot" style="grid-area: resetLink; place-self: end;">Reset password</a>
<a href="forgot" style="grid-area: resetLink; place-self: end;">Reset password</a>
}
<button style="grid-area: button" @onclick="DoLoginAsync">Login</button>
<button style="grid-area: button" type="submit">@(isEmailSubmitted ? "Login" : "Next")</button>
</NotAuthorized>
</AuthorizeView>
</section>
</form>
</main>
@@ -41,21 +47,49 @@
private string email = string.Empty;
private string password = string.Empty;
private string[] errorList = [];
private System.Threading.CancellationTokenSource? _clearErrorCts;
private bool isEmailSubmitted = false;
private Guid errorKey;
public async Task DoLoginAsync()
private async Task HandleSubmit()
{
errorList = [];
if (!isEmailSubmitted)
{
SubmitEmail();
}
else
{
await DoLoginAsync();
}
}
private void SubmitEmail()
{
SetErrors([]);
if (string.IsNullOrWhiteSpace(email))
{
errorList = ["Email is required."];
SetErrors(["Email is required."]);
return;
}
isEmailSubmitted = true;
}
public async Task DoLoginAsync()
{
SetErrors([]);
if (string.IsNullOrWhiteSpace(email))
{
SetErrors(["Email is required."]);
return;
}
if (string.IsNullOrWhiteSpace(password))
{
errorList = ["Password is required."];
SetErrors(["Password is required."]);
return;
}
@@ -70,7 +104,34 @@
}
else
{
errorList = result.ErrorList;
SetErrors(result.ErrorList);
}
}
private void SetErrors(string[] errors)
{
_clearErrorCts?.Cancel();
errorList = errors;
if (errors.Length > 0)
{
errorKey = Guid.NewGuid();
_clearErrorCts = new System.Threading.CancellationTokenSource();
_ = ClearErrorsAfterDelay(_clearErrorCts.Token);
}
}
private async Task ClearErrorsAfterDelay(System.Threading.CancellationToken token)
{
try
{
await Task.Delay(10000, token);
errorList = [];
await InvokeAsync(StateHasChanged);
}
catch (TaskCanceledException)
{
// Ignore
}
}
}

View File

@@ -1,20 +1,52 @@
main {
padding: 1rem;
display: grid;
place-content: flex-start center;
}
.LoginForm {
grid-area: form;
display: inline-grid;
grid-template-areas:
"h1 h1"
"errors errors"
"emailLabel emailControl"
"passLabel passControl"
". resetLink"
"button button";
gap: 0.5rem 3rem;
color: var(--backgroundColor);
background-color: var(--middlegroundColor);
padding: 2rem;
}
.LoginForm a {
color: var(--middlegroundHrefColor);
}
.LoginForm .Errors {
color: darkred;
background-color: var(--foregroundColor);
display: flex;
flex-direction: column;
}
.LoginForm .Errors ul {
margin: 0.5rem 0;
}
.ErrorProgress {
height: 5px;
background-color: darkred;
width: 100%;
animation: deplete 10s linear forwards;
}
@keyframes deplete {
from {
width: 100%;
}
to {
width: 0%;
}
}

View File

@@ -12,10 +12,13 @@
<span></span>
</row>
<hr />
@foreach (var session in allSessions)
@foreach (var session in allSessions)
{
<row>
<GameBrowserEntry Session="session" OnSessionDeleted="FetchSessions" />
<GameBrowserEntry Session="session"
OnSessionDeleted="FetchSessions"
OnSessionSelected="HandleSessionSelected"
IsSelected="SelectedSession?.SessionId == session.SessionId" />
</row>
}
</div>
@@ -29,6 +32,9 @@
@code {
private SessionMetadata[] allSessions = Array.Empty<SessionMetadata>();
[Parameter] public SessionMetadata? SelectedSession { get; set; }
[Parameter] public EventCallback<SessionMetadata> OnSessionSelected { get; set; }
protected override Task OnInitializedAsync()
{
return FetchSessions();
@@ -44,4 +50,9 @@
StateHasChanged();
}
}
private async Task HandleSessionSelected(SessionMetadata session)
{
await OnSessionSelected.InvokeAsync(session);
}
}

View File

@@ -1,31 +1,31 @@
@inject ShogiService Service
@using System.Security.Claims
<gameBrowserEntry>
@if (showDeletePrompt)
{
<modal class="PrimaryTheme ThemeVariant--Contrast">
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
@if (showDeleteError)
{
<p style="color: darkred;">An error occurred.</p>
<div style="flex: 1;" />
<button @onclick="HideModal">Cancel</button>
}
else
{
<p>Do you wish to delete this session?</p>
<div style="flex: 1;" />
<button @onclick="HideModal">No</button>
<button @onclick="DeleteSession">Yes</button>
}
</div>
</modal>
}
<gameBrowserEntry @onclick="HandleSessionSelectedClick" data-selected="@IsSelected">
@if (showDeletePrompt)
{
<modal class="PrimaryTheme ThemeVariant--Contrast">
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
@if (showDeleteError)
{
<p style="color: darkred;">An error occurred.</p>
<div style="flex: 1;" />
<button @onclick="HideModal">Cancel</button>
}
else
{
<p>Do you wish to delete this session?</p>
<div style="flex: 1;" />
<button @onclick="HideModal">No</button>
<button @onclick="DeleteSession">Yes</button>
}
</div>
</modal>
}
<div>
<a href="play/@Session.SessionId">@Session.Player1</a>
</div>
<div>
<span>@Session.Player1</span>
</div>
@if (string.IsNullOrEmpty(Session.Player2))
{
<span>1 / 2</span>
@@ -50,9 +50,16 @@
[Parameter][EditorRequired] public SessionMetadata Session { get; set; } = default!;
[Parameter][EditorRequired] public EventCallback OnSessionDeleted { get; set; }
[Parameter] public EventCallback<SessionMetadata> OnSessionSelected { get; set; }
[Parameter] public bool IsSelected { get; set; }
private bool showDeletePrompt = false;
private bool showDeleteError = false;
private async Task HandleSessionSelectedClick()
{
await OnSessionSelected.InvokeAsync(Session);
}
void HideModal()
{

View File

@@ -5,8 +5,18 @@
padding-left: 5px; /* Matches box shadow on hover */
gap: 1rem;
place-items: center start;
cursor: pointer;
}
gameBrowserEntry:hover {
background-color: rgba(255, 255, 255, 0.1);
}
gameBrowserEntry[data-selected="true"] {
background-color: rgba(255, 255, 255, 0.2);
box-shadow: inset 3px 0 0 0 var(--primary-color, #fff);
}
modal {
position: absolute;
top: 0;

View File

@@ -1,11 +1,57 @@
@page "/search"
@inject ShogiService Service
@inject NavigationManager Navigation
<main class="SearchPage PrimaryTheme">
<h3>Find Sessions</h3>
<GameBrowser />
<div class="search-content">
<GameBrowser SelectedSession="selectedSession" OnSessionSelected="HandleSessionSelected" />
<aside class="preview-panel PrimaryTheme ThemeVariant--Contrast">
@if (selectedSession is not null)
{
@if (previewSession is not null)
{
<GameBoardPresentation Session="previewSession"
Perspective="WhichPlayer.Player1"
UseSideboard="true" />
}
else
{
<p>Loading preview...</p>
}
<div class="preview-actions">
<button @onclick="JoinGame">Join Game</button>
</div>
}
else
{
<p class="no-selection">Select a game to preview</p>
}
</aside>
</div>
</main>
@code {
private SessionMetadata? selectedSession;
private Session? previewSession;
private async Task HandleSessionSelected(SessionMetadata session)
{
selectedSession = session;
previewSession = null;
previewSession = await Service.GetSession(session.SessionId.ToString());
}
private void JoinGame()
{
if (selectedSession is not null)
{
Navigation.NavigateTo($"/play/{selectedSession.SessionId}");
}
}
}

View File

@@ -1,3 +1,39 @@
.SearchPage {
padding: 0 0.5rem;
display: grid;
grid-template-columns: 100%;
grid-template-rows: auto 1fr;
}
.search-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: start;
}
.preview-panel {
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
min-height: 300px;
}
.preview-panel .no-selection {
color: #888;
font-style: italic;
}
.preview-actions {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1rem;
}
.preview-actions button {
padding: 0.5rem 1.5rem;
font-size: 1rem;
}

View File

@@ -3,6 +3,7 @@
--middlegroundColor: #D1D1D1;
--foregroundColor: #eaeaea;
--hrefColor: #99c3ff;
--middlegroundHrefColor: #0065be;
--uniformBottomMargin: 0.5rem;
background-color: var(--backgroundColor);
color: var(--foregroundColor);