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 @inject NavigationManager navigator
<main class="PrimaryTheme"> <main class="PrimaryTheme">
<h1>Login</h1>
<section class="LoginForm"> <form class="LoginForm" @onsubmit="HandleSubmit">
<h1 style="grid-area: h1">Login</h1>
<AuthorizeView> <AuthorizeView>
<Authorized> <Authorized>
<div>You're logged in as @context.User.Identity?.Name.</div> <div>You're logged in as @context.User.Identity?.Name.</div>
@@ -13,26 +13,32 @@
<NotAuthorized> <NotAuthorized>
@if (errorList.Length > 0) @if (errorList.Length > 0)
{ {
<ul class="Errors" style="grid-area: errors"> <div class="Errors" style="grid-area: errors">
<ul>
@foreach (var error in errorList) @foreach (var error in errorList)
{ {
<li>@error</li> <li>@error</li>
} }
</ul> </ul>
<div class="ErrorProgress" @key="errorKey"></div>
</div>
} }
<label for="email" style="grid-area: emailLabel">Email</label> <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" />
@if (isEmailSubmitted)
{
<label for="password" style="grid-area: passLabel">Password</label> <label for="password" style="grid-area: passLabel">Password</label>
<input required id="password" name="passwordInput" type="password" style="grid-area: passControl" @bind-value="password" /> <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> </NotAuthorized>
</AuthorizeView> </AuthorizeView>
</section> </form>
</main> </main>
@@ -41,21 +47,49 @@
private string email = string.Empty; private string email = string.Empty;
private string password = string.Empty; private string password = string.Empty;
private string[] errorList = []; 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)) 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; return;
} }
if (string.IsNullOrWhiteSpace(password)) if (string.IsNullOrWhiteSpace(password))
{ {
errorList = ["Password is required."]; SetErrors(["Password is required."]);
return; return;
} }
@@ -70,7 +104,34 @@
} }
else 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 { main {
padding: 1rem; padding: 1rem;
display: grid;
place-content: flex-start center;
} }
.LoginForm { .LoginForm {
grid-area: form; grid-area: form;
display: inline-grid; display: inline-grid;
grid-template-areas: grid-template-areas:
"h1 h1"
"errors errors" "errors errors"
"emailLabel emailControl" "emailLabel emailControl"
"passLabel passControl" "passLabel passControl"
". resetLink" ". resetLink"
"button button"; "button button";
gap: 0.5rem 3rem; gap: 0.5rem 3rem;
color: var(--backgroundColor);
background-color: var(--middlegroundColor);
padding: 2rem;
}
.LoginForm a {
color: var(--middlegroundHrefColor);
} }
.LoginForm .Errors { .LoginForm .Errors {
color: darkred; color: darkred;
background-color: var(--foregroundColor); 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

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

View File

@@ -1,7 +1,7 @@
@inject ShogiService Service @inject ShogiService Service
@using System.Security.Claims @using System.Security.Claims
<gameBrowserEntry> <gameBrowserEntry @onclick="HandleSessionSelectedClick" data-selected="@IsSelected">
@if (showDeletePrompt) @if (showDeletePrompt)
{ {
<modal class="PrimaryTheme ThemeVariant--Contrast"> <modal class="PrimaryTheme ThemeVariant--Contrast">
@@ -24,7 +24,7 @@
} }
<div> <div>
<a href="play/@Session.SessionId">@Session.Player1</a> <span>@Session.Player1</span>
</div> </div>
@if (string.IsNullOrEmpty(Session.Player2)) @if (string.IsNullOrEmpty(Session.Player2))
{ {
@@ -50,9 +50,16 @@
[Parameter][EditorRequired] public SessionMetadata Session { get; set; } = default!; [Parameter][EditorRequired] public SessionMetadata Session { get; set; } = default!;
[Parameter][EditorRequired] public EventCallback OnSessionDeleted { get; set; } [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 showDeletePrompt = false;
private bool showDeleteError = false; private bool showDeleteError = false;
private async Task HandleSessionSelectedClick()
{
await OnSessionSelected.InvokeAsync(Session);
}
void HideModal() void HideModal()
{ {

View File

@@ -5,6 +5,16 @@
padding-left: 5px; /* Matches box shadow on hover */ padding-left: 5px; /* Matches box shadow on hover */
gap: 1rem; gap: 1rem;
place-items: center start; 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 { modal {

View File

@@ -1,11 +1,57 @@
@page "/search" @page "/search"
@inject ShogiService Service
@inject NavigationManager Navigation
<main class="SearchPage PrimaryTheme"> <main class="SearchPage PrimaryTheme">
<h3>Find Sessions</h3> <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> </main>
@code { @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 { .SearchPage {
padding: 0 0.5rem; 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; --middlegroundColor: #D1D1D1;
--foregroundColor: #eaeaea; --foregroundColor: #eaeaea;
--hrefColor: #99c3ff; --hrefColor: #99c3ff;
--middlegroundHrefColor: #0065be;
--uniformBottomMargin: 0.5rem; --uniformBottomMargin: 0.5rem;
background-color: var(--backgroundColor); background-color: var(--backgroundColor);
color: var(--foregroundColor); color: var(--foregroundColor);