checkpoint
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
@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">
|
||||||
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
|
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
|
||||||
@if (showDeleteError)
|
@if (showDeleteError)
|
||||||
{
|
{
|
||||||
<p style="color: darkred;">An error occurred.</p>
|
<p style="color: darkred;">An error occurred.</p>
|
||||||
<div style="flex: 1;" />
|
<div style="flex: 1;" />
|
||||||
<button @onclick="HideModal">Cancel</button>
|
<button @onclick="HideModal">Cancel</button>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<p>Do you wish to delete this session?</p>
|
<p>Do you wish to delete this session?</p>
|
||||||
<div style="flex: 1;" />
|
<div style="flex: 1;" />
|
||||||
<button @onclick="HideModal">No</button>
|
<button @onclick="HideModal">No</button>
|
||||||
<button @onclick="DeleteSession">Yes</button>
|
<button @onclick="DeleteSession">Yes</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
}
|
}
|
||||||
|
|
||||||
<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))
|
||||||
{
|
{
|
||||||
<span>1 / 2</span>
|
<span>1 / 2</span>
|
||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,18 @@
|
|||||||
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 {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user