diff --git a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor
index 24b1bbe..8befcab 100644
--- a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor
+++ b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor
@@ -3,9 +3,9 @@
@inject NavigationManager navigator
- Login
-
+
@@ -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
}
}
}
\ No newline at end of file
diff --git a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css
index 42ff5c7..f86dbcd 100644
--- a/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css
+++ b/Shogi/FrontEnd/Components/Pages/Identity/LoginPage.razor.css
@@ -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%;
+ }
+}
diff --git a/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor
index 7866211..63028cf 100644
--- a/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor
+++ b/Shogi/FrontEnd/Components/Pages/Play/GameBrowser.razor
@@ -12,10 +12,13 @@
- @foreach (var session in allSessions)
+ @foreach (var session in allSessions)
{
-
+
}
@@ -29,6 +32,9 @@
@code {
private SessionMetadata[] allSessions = Array.Empty();
+ [Parameter] public SessionMetadata? SelectedSession { get; set; }
+ [Parameter] public EventCallback OnSessionSelected { get; set; }
+
protected override Task OnInitializedAsync()
{
return FetchSessions();
@@ -44,4 +50,9 @@
StateHasChanged();
}
}
+
+ private async Task HandleSessionSelected(SessionMetadata session)
+ {
+ await OnSessionSelected.InvokeAsync(session);
+ }
}
diff --git a/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor
index ab69b0a..b5b65d6 100644
--- a/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor
+++ b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor
@@ -1,31 +1,31 @@
@inject ShogiService Service
@using System.Security.Claims
-
- @if (showDeletePrompt)
- {
-
-
- @if (showDeleteError)
- {
-
An error occurred.
-
-
- }
- else
- {
-
Do you wish to delete this session?
-
-
-
- }
-
-
- }
+
+@if (showDeletePrompt)
+{
+
+
+@if (showDeleteError)
+{
+
An error occurred.
+
+
+}
+else
+{
+
Do you wish to delete this session?
+
+
+
+}
+
+
+}
-
+
+@Session.Player1
+
@if (string.IsNullOrEmpty(Session.Player2))
{
1 / 2
@@ -50,9 +50,16 @@
[Parameter][EditorRequired] public SessionMetadata Session { get; set; } = default!;
[Parameter][EditorRequired] public EventCallback OnSessionDeleted { get; set; }
+ [Parameter] public EventCallback 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()
{
diff --git a/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css
index 9c18c06..b00c83f 100644
--- a/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css
+++ b/Shogi/FrontEnd/Components/Pages/Play/GameBrowserEntry.razor.css
@@ -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;
diff --git a/Shogi/FrontEnd/Components/Pages/SearchPage.razor b/Shogi/FrontEnd/Components/Pages/SearchPage.razor
index 9d3afd5..d957021 100644
--- a/Shogi/FrontEnd/Components/Pages/SearchPage.razor
+++ b/Shogi/FrontEnd/Components/Pages/SearchPage.razor
@@ -1,11 +1,57 @@
@page "/search"
+@inject ShogiService Service
+@inject NavigationManager Navigation
+
Find Sessions
-
+
+
+
+
+
@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}");
+ }
+ }
}
diff --git a/Shogi/FrontEnd/Components/Pages/SearchPage.razor.css b/Shogi/FrontEnd/Components/Pages/SearchPage.razor.css
index f14027d..58cbf6f 100644
--- a/Shogi/FrontEnd/Components/Pages/SearchPage.razor.css
+++ b/Shogi/FrontEnd/Components/Pages/SearchPage.razor.css
@@ -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;
}
diff --git a/Shogi/wwwroot/css/themes.css b/Shogi/wwwroot/css/themes.css
index 216e2e7..ea72a42 100644
--- a/Shogi/wwwroot/css/themes.css
+++ b/Shogi/wwwroot/css/themes.css
@@ -3,6 +3,7 @@
--middlegroundColor: #D1D1D1;
--foregroundColor: #eaeaea;
--hrefColor: #99c3ff;
+ --middlegroundHrefColor: #0065be;
--uniformBottomMargin: 0.5rem;
background-color: var(--backgroundColor);
color: var(--foregroundColor);