squash a bunch of commits
This commit is contained in:
138
Shogi.UI/Pages/Home/Account/AccountManager.cs
Normal file
138
Shogi.UI/Pages/Home/Account/AccountManager.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Shogi.UI.Pages.Home.Api;
|
||||
using Shogi.UI.Shared;
|
||||
|
||||
namespace Shogi.UI.Pages.Home.Account;
|
||||
|
||||
public class AccountManager
|
||||
{
|
||||
private readonly AccountState accountState;
|
||||
private readonly IShogiApi shogiApi;
|
||||
private readonly IConfiguration configuration;
|
||||
private readonly ILocalStorage localStorage;
|
||||
//private readonly AuthenticationStateProvider authState;
|
||||
private readonly NavigationManager navigation;
|
||||
private readonly ShogiSocket shogiSocket;
|
||||
|
||||
public AccountManager(
|
||||
AccountState accountState,
|
||||
IShogiApi unauthenticatedClient,
|
||||
IConfiguration configuration,
|
||||
//AuthenticationStateProvider authState,
|
||||
ILocalStorage localStorage,
|
||||
NavigationManager navigation,
|
||||
ShogiSocket shogiSocket)
|
||||
{
|
||||
this.accountState = accountState;
|
||||
this.shogiApi = unauthenticatedClient;
|
||||
this.configuration = configuration;
|
||||
//this.authState = authState;
|
||||
this.localStorage = localStorage;
|
||||
this.navigation = navigation;
|
||||
this.shogiSocket = shogiSocket;
|
||||
}
|
||||
|
||||
private User? User { get => accountState.User; set => accountState.User = value; }
|
||||
|
||||
public async Task LoginWithGuestAccount()
|
||||
{
|
||||
var response = await shogiApi.GetGuestToken();
|
||||
if (response != null)
|
||||
{
|
||||
User = new User
|
||||
{
|
||||
DisplayName = response.DisplayName,
|
||||
Id = response.UserId,
|
||||
OneTimeSocketToken = response.OneTimeToken,
|
||||
WhichAccountPlatform = WhichAccountPlatform.Guest
|
||||
};
|
||||
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
||||
await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoginWithMicrosoftAccount()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
//var state = await authState.GetAuthenticationStateAsync();
|
||||
|
||||
//if (state.User?.Identity?.Name == null || state.User?.Identity?.IsAuthenticated != true)
|
||||
//{
|
||||
// navigation.NavigateTo("authentication/login");
|
||||
// return;
|
||||
//}
|
||||
|
||||
//var id = state.User.Identity.Name;
|
||||
//var socketToken = await shogiApi.GetToken();
|
||||
//if (socketToken.HasValue)
|
||||
//{
|
||||
// User = new User
|
||||
// {
|
||||
// DisplayName = id,
|
||||
// Id = id,
|
||||
// OneTimeSocketToken = socketToken.Value
|
||||
// };
|
||||
|
||||
// await ConnectToSocketAsync();
|
||||
// await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
|
||||
// }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to log in with the account used from the previous browser session.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> TryLoginSilentAsync()
|
||||
{
|
||||
var platform = await localStorage.GetAccountPlatform();
|
||||
if (platform == WhichAccountPlatform.Guest)
|
||||
{
|
||||
var response = await shogiApi.GetGuestToken();
|
||||
if (response != null)
|
||||
{
|
||||
User = new User
|
||||
{
|
||||
DisplayName = response.DisplayName,
|
||||
Id = response.UserId,
|
||||
OneTimeSocketToken = response.OneTimeToken,
|
||||
WhichAccountPlatform = WhichAccountPlatform.Guest
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (platform == WhichAccountPlatform.Microsoft)
|
||||
{
|
||||
Console.WriteLine("Login Microsoft");
|
||||
throw new NotImplementedException();
|
||||
//var state = await authState.GetAuthenticationStateAsync();
|
||||
//if (state.User?.Identity?.Name != null)
|
||||
//{
|
||||
// var id = state.User.Identity;
|
||||
// User = new User
|
||||
// {
|
||||
// DisplayName = id.Name,
|
||||
// Id = id.Name
|
||||
// };
|
||||
// var token = await shogiApi.GetToken();
|
||||
// if (token.HasValue)
|
||||
// {
|
||||
// User.OneTimeSocketToken = token.Value;
|
||||
// }
|
||||
//}
|
||||
// TODO: If this fails then platform saved to localStorage should get cleared
|
||||
}
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
await Task.WhenAll(shogiApi.GuestLogout(), localStorage.DeleteAccountPlatform());
|
||||
User = null;
|
||||
}
|
||||
}
|
||||
28
Shogi.UI/Pages/Home/Account/AccountState.cs
Normal file
28
Shogi.UI/Pages/Home/Account/AccountState.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Shogi.UI.Pages.Home.Account;
|
||||
|
||||
public class AccountState
|
||||
{
|
||||
public event EventHandler<LoginEventArgs>? LoginChangedEvent;
|
||||
|
||||
private User? user;
|
||||
public User? User
|
||||
{
|
||||
get => user;
|
||||
set
|
||||
{
|
||||
if (user != value)
|
||||
{
|
||||
user = value;
|
||||
EmitLoginChangedEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitLoginChangedEvent()
|
||||
{
|
||||
LoginChangedEvent?.Invoke(this, new LoginEventArgs
|
||||
{
|
||||
User = User
|
||||
});
|
||||
}
|
||||
}
|
||||
24
Shogi.UI/Pages/Home/Account/LocalStorageExtensions.cs
Normal file
24
Shogi.UI/Pages/Home/Account/LocalStorageExtensions.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Shogi.UI.Shared;
|
||||
|
||||
namespace Shogi.UI.Pages.Home.Account
|
||||
{
|
||||
public static class LocalStorageExtensions
|
||||
{
|
||||
private const string AccountPlatform = "AccountPlatform";
|
||||
|
||||
public static Task<WhichAccountPlatform?> GetAccountPlatform(this ILocalStorage self)
|
||||
{
|
||||
return self.Get<WhichAccountPlatform>(AccountPlatform).AsTask();
|
||||
}
|
||||
|
||||
public static Task SetAccountPlatform(this ILocalStorage self, WhichAccountPlatform platform)
|
||||
{
|
||||
return self.Set(AccountPlatform, platform.ToString()).AsTask();
|
||||
}
|
||||
|
||||
public static Task DeleteAccountPlatform(this ILocalStorage self)
|
||||
{
|
||||
return self.Delete(AccountPlatform).AsTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Shogi.UI/Pages/Home/Account/LoginEventArgs.cs
Normal file
7
Shogi.UI/Pages/Home/Account/LoginEventArgs.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Shogi.UI.Pages.Home.Account
|
||||
{
|
||||
public class LoginEventArgs : EventArgs
|
||||
{
|
||||
public User? User { get; set; }
|
||||
}
|
||||
}
|
||||
12
Shogi.UI/Pages/Home/Account/User.cs
Normal file
12
Shogi.UI/Pages/Home/Account/User.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Shogi.UI.Pages.Home.Account
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
public WhichAccountPlatform WhichAccountPlatform { get; set; }
|
||||
|
||||
public Guid OneTimeSocketToken { get; set; }
|
||||
}
|
||||
}
|
||||
8
Shogi.UI/Pages/Home/Account/WhichAccountPlatform.cs
Normal file
8
Shogi.UI/Pages/Home/Account/WhichAccountPlatform.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Shogi.UI.Pages.Home.Account
|
||||
{
|
||||
public enum WhichAccountPlatform
|
||||
{
|
||||
Guest,
|
||||
Microsoft
|
||||
}
|
||||
}
|
||||
18
Shogi.UI/Pages/Home/Api/CookieMessageHandler.cs
Normal file
18
Shogi.UI/Pages/Home/Api/CookieMessageHandler.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Http;
|
||||
|
||||
namespace Shogi.UI.Pages.Home.Api
|
||||
{
|
||||
public class CookieCredentialsMessageHandler : DelegatingHandler
|
||||
{
|
||||
|
||||
public CookieCredentialsMessageHandler()
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Shogi.UI/Pages/Home/Api/IShogiApi.cs
Normal file
17
Shogi.UI/Pages/Home/Api/IShogiApi.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Shogi.Contracts.Api;
|
||||
using Shogi.Contracts.Types;
|
||||
using System.Net;
|
||||
|
||||
namespace Shogi.UI.Pages.Home.Api
|
||||
{
|
||||
public interface IShogiApi
|
||||
{
|
||||
Task<CreateGuestTokenResponse?> GetGuestToken();
|
||||
Task<Session?> GetSession(string name);
|
||||
Task<ReadAllSessionsResponse?> GetSessions();
|
||||
Task<Guid?> GetToken();
|
||||
Task GuestLogout();
|
||||
Task PostMove(string sessionName, Move move);
|
||||
Task<HttpStatusCode> PostSession(string name, bool isPrivate);
|
||||
}
|
||||
}
|
||||
21
Shogi.UI/Pages/Home/Api/MsalMessageHandler.cs
Normal file
21
Shogi.UI/Pages/Home/Api/MsalMessageHandler.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||
|
||||
namespace Shogi.UI.Pages.Home.Api
|
||||
{
|
||||
public class MsalMessageHandler : AuthorizationMessageHandler
|
||||
{
|
||||
public MsalMessageHandler(IAccessTokenProvider provider, NavigationManager navigation) : base(provider, navigation)
|
||||
{
|
||||
ConfigureHandler(
|
||||
authorizedUrls: new[] { "https://api.lucaserver.space", "https://localhost:5001" },
|
||||
scopes: new[] { "api://c1e94676-cab0-42ba-8b6c-9532b8486fff/DefaultScope" },
|
||||
returnUrl: "https://localhost:3000");
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Shogi.UI/Pages/Home/Api/ShogiApi.cs
Normal file
98
Shogi.UI/Pages/Home/Api/ShogiApi.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Shogi.Contracts.Api;
|
||||
using Shogi.Contracts.Types;
|
||||
using Shogi.UI.Pages.Home.Account;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Shogi.UI.Pages.Home.Api
|
||||
{
|
||||
public class ShogiApi : IShogiApi
|
||||
{
|
||||
public const string GuestClientName = "Guest";
|
||||
public const string MsalClientName = "Msal";
|
||||
public const string AnonymouseClientName = "Anonymous";
|
||||
|
||||
private readonly JsonSerializerOptions serializerOptions;
|
||||
private readonly IHttpClientFactory clientFactory;
|
||||
private readonly AccountState accountState;
|
||||
|
||||
public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState)
|
||||
{
|
||||
serializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
};
|
||||
serializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
this.clientFactory = clientFactory;
|
||||
this.accountState = accountState;
|
||||
}
|
||||
|
||||
private HttpClient HttpClient => accountState.User?.WhichAccountPlatform switch
|
||||
{
|
||||
WhichAccountPlatform.Guest => clientFactory.CreateClient(GuestClientName),
|
||||
WhichAccountPlatform.Microsoft => clientFactory.CreateClient(MsalClientName),
|
||||
_ => clientFactory.CreateClient(AnonymouseClientName)
|
||||
};
|
||||
|
||||
public async Task<CreateGuestTokenResponse?> GetGuestToken()
|
||||
{
|
||||
var response = await HttpClient.GetAsync(new Uri("User/GuestToken", UriKind.Relative));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<CreateGuestTokenResponse>(serializerOptions);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task GuestLogout()
|
||||
{
|
||||
var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<Session?> GetSession(string name)
|
||||
{
|
||||
var response = await HttpClient.GetAsync(new Uri($"Session/{name}", UriKind.Relative));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return (await response.Content.ReadFromJsonAsync<ReadSessionResponse>(serializerOptions))?.Session;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<ReadAllSessionsResponse?> GetSessions()
|
||||
{
|
||||
var response = await HttpClient.GetAsync(new Uri("Session", UriKind.Relative));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<ReadAllSessionsResponse>(serializerOptions);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<Guid?> GetToken()
|
||||
{
|
||||
var response = await HttpClient.GetAsync(new Uri("User/Token", UriKind.Relative));
|
||||
var deserialized = await response.Content.ReadFromJsonAsync<CreateTokenResponse>(serializerOptions);
|
||||
return deserialized?.OneTimeToken;
|
||||
}
|
||||
|
||||
public async Task PostMove(string sessionName, Contracts.Types.Move move)
|
||||
{
|
||||
await this.HttpClient.PostAsJsonAsync($"{sessionName}/Move", new MovePieceCommand { Move = move });
|
||||
}
|
||||
|
||||
public async Task<HttpStatusCode> PostSession(string name, bool isPrivate)
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand
|
||||
{
|
||||
Name = name,
|
||||
IsPrivate = isPrivate
|
||||
});
|
||||
return response.StatusCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Shogi.UI/Pages/Home/GameBoard.razor
Normal file
62
Shogi.UI/Pages/Home/GameBoard.razor
Normal file
@@ -0,0 +1,62 @@
|
||||
@using Shogi.Contracts.Types;
|
||||
@inject IShogiApi ShogiApi
|
||||
@inject AccountState Account;
|
||||
|
||||
<section class="game-board" data-perspective="@Perspective">
|
||||
@for (var rank = 9; rank > 0; rank--)
|
||||
{
|
||||
foreach (var file in Files)
|
||||
{
|
||||
var position = $"{file}{rank}";
|
||||
var piece = session?.BoardState.Board[position];
|
||||
<div class="tile" data-position="@(position)" style="grid-area: @(position)" @onclick="() => OnClickTile(piece, position)">
|
||||
<GamePiece Piece="piece" Perspective="@Perspective" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="ruler vertical" style="grid-area: rank">
|
||||
<span>9</span>
|
||||
<span>8</span>
|
||||
<span>7</span>
|
||||
<span>6</span>
|
||||
<span>5</span>
|
||||
<span>4</span>
|
||||
<span>3</span>
|
||||
<span>2</span>
|
||||
<span>1</span>
|
||||
</div>
|
||||
<div class="ruler" style="grid-area: file">
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
<span>C</span>
|
||||
<span>D</span>
|
||||
<span>E</span>
|
||||
<span>F</span>
|
||||
<span>G</span>
|
||||
<span>H</span>
|
||||
<span>I</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? SessionName { get; set; }
|
||||
static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
|
||||
WhichPlayer Perspective => Account.User?.Id == session?.Player1 ? WhichPlayer.Player1 : WhichPlayer.Player2;
|
||||
|
||||
Session? session;
|
||||
string? selectedPosition;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(SessionName))
|
||||
{
|
||||
this.session = await ShogiApi.GetSession(SessionName);
|
||||
}
|
||||
}
|
||||
|
||||
void OnClickTile(Piece? piece, string position)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
63
Shogi.UI/Pages/Home/GameBoard.razor.css
Normal file
63
Shogi.UI/Pages/Home/GameBoard.razor.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.game-board {
|
||||
background-color: #444;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"rank A9 B9 C9 D9 E9 F9 G9 H9 I9"
|
||||
"rank A8 B8 C8 D8 E8 F8 G8 H8 I8"
|
||||
"rank A7 B7 C7 D7 E7 F7 G7 H7 I7"
|
||||
"rank A6 B6 C6 D6 E6 F6 G6 H6 I6"
|
||||
"rank A5 B5 C5 D5 E5 F5 G5 H5 I5"
|
||||
"rank A4 B4 C4 D4 E4 F4 G4 H4 I4"
|
||||
"rank A3 B3 C3 D3 E3 F3 G3 H3 I3"
|
||||
"rank A2 B2 C2 D2 E2 F2 G2 H2 I2"
|
||||
"rank A1 B1 C1 D1 E1 F1 G1 H1 I1"
|
||||
". file file file file file file file file file";
|
||||
grid-template-columns: auto repeat(9, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(9, minmax(0, 1fr)) auto;
|
||||
max-height: calc(100vh - 3rem);
|
||||
width: calc(100vmin * 0.9167);
|
||||
aspect-ratio: 0.9167;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.game-board[data-perspective="Player2"] {
|
||||
grid-template-areas:
|
||||
"file file file file file file file file file ."
|
||||
"I1 H1 G1 F1 E1 D1 C1 B1 A1 rank"
|
||||
"I2 H2 G2 F2 E2 D2 C2 B2 A2 rank"
|
||||
"I3 H3 G3 F3 E3 D3 C3 B3 A3 rank"
|
||||
"I4 H4 G4 F4 E4 D4 C4 B4 A4 rank"
|
||||
"I5 H5 G5 F5 E5 D5 C5 B5 A5 rank"
|
||||
"I6 H6 G6 F6 E6 D6 C6 B6 A6 rank"
|
||||
"I7 H7 G7 F7 E7 D7 C7 B7 A7 rank"
|
||||
"I8 H8 G8 F8 E8 D8 C8 B8 A8 rank"
|
||||
"I9 H9 G9 F9 E9 D9 C9 B9 A9 rank";
|
||||
grid-template-columns: repeat(9, minmax(0, 1fr)) auto;
|
||||
grid-template-rows: auto repeat(9, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.tile {
|
||||
background-color: beige;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.ruler {
|
||||
color: beige;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.ruler.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.game-board[data-perspective="Player2"] .ruler {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.game-board[data-perspective="Player2"] .ruler.vertical {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
109
Shogi.UI/Pages/Home/GameBrowser.razor
Normal file
109
Shogi.UI/Pages/Home/GameBrowser.razor
Normal file
@@ -0,0 +1,109 @@
|
||||
@using Shogi.Contracts.Types
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Net
|
||||
@inject IShogiApi ShogiApi;
|
||||
@inject ShogiSocket ShogiSocket;
|
||||
|
||||
<section class="game-browser">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#search-pane">Search</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#create-pane">Create</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="search-pane">
|
||||
<div class="list-group">
|
||||
@if (!sessions.Any())
|
||||
{
|
||||
<p>No games exist</p>
|
||||
}
|
||||
@foreach (var session in sessions)
|
||||
{
|
||||
<button class="list-group-item list-group-item-action @ActiveCss(session)" @onclick="() => OnClickSession(session)">
|
||||
<span>@session.Name</span>
|
||||
<span>(@session.PlayerCount/2)</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="create-pane">
|
||||
<EditForm Model="createForm" OnValidSubmit="async () => await CreateSession()">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<h3>Start a new session</h3>
|
||||
<div class="form-floating mb-3">
|
||||
<InputText type="text" class="form-control" id="session-name" placeholder="Session name" @bind-Value="createForm.Name" />
|
||||
<label for="session-name">Session name</label>
|
||||
</div>
|
||||
<div class="flex-between mb-3">
|
||||
<div class="form-check">
|
||||
<InputCheckbox class="form-check-input" role="switch" id="session-privacy" @bind-Value="createForm.IsPrivate" />
|
||||
<label class="form-check-label" for="session-privacy">Private?</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
|
||||
@if (createSessionStatusCode == HttpStatusCode.Created)
|
||||
{
|
||||
<div class="alert alert-success" role="alert">
|
||||
Session started. View it in the search tab.
|
||||
</div>
|
||||
}
|
||||
else if (createSessionStatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
<div class="alert alert-warning" role="alert">
|
||||
The name you chose is taken; choose another.
|
||||
</div>
|
||||
}
|
||||
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public Action<SessionMetadata>? ActiveSessionChanged { get; set; }
|
||||
CreateForm createForm = new();
|
||||
SessionMetadata[] sessions = Array.Empty<SessionMetadata>();
|
||||
SessionMetadata? activeSession;
|
||||
HttpStatusCode? createSessionStatusCode;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ShogiSocket.OnCreateGameMessage += async (sender, message) => await FetchSessions();
|
||||
await FetchSessions();
|
||||
}
|
||||
string ActiveCss(SessionMetadata s) => s == activeSession ? "active" : string.Empty;
|
||||
|
||||
void OnClickSession(SessionMetadata s)
|
||||
{
|
||||
activeSession = s;
|
||||
ActiveSessionChanged?.Invoke(s);
|
||||
}
|
||||
|
||||
async Task FetchSessions()
|
||||
{
|
||||
var sessions = await ShogiApi.GetSessions();
|
||||
if (sessions != null)
|
||||
{
|
||||
this.sessions = sessions.PlayerHasJoinedSessions.Concat(sessions.AllOtherSessions).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
async Task CreateSession()
|
||||
{
|
||||
createSessionStatusCode = await ShogiApi.PostSession(createForm.Name, createForm.IsPrivate);
|
||||
}
|
||||
|
||||
private class CreateForm
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsPrivate { get; set; }
|
||||
}
|
||||
}
|
||||
13
Shogi.UI/Pages/Home/GameBrowser.razor.css
Normal file
13
Shogi.UI/Pages/Home/GameBrowser.razor.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.game-browser {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--contrast-color);
|
||||
}
|
||||
|
||||
#search-pane button.list-group-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
#search-pane button.list-group-item.active {
|
||||
background-color: #444;
|
||||
border-color: #666;
|
||||
}
|
||||
44
Shogi.UI/Pages/Home/GamePiece.razor
Normal file
44
Shogi.UI/Pages/Home/GamePiece.razor
Normal file
@@ -0,0 +1,44 @@
|
||||
@using Shogi.Contracts.Types
|
||||
|
||||
<div data-upsidedown="@(Piece?.Owner != Perspective)" data-owner="@Piece?.Owner.ToString()">
|
||||
@switch (Piece?.WhichPiece)
|
||||
{
|
||||
case WhichPiece.Bishop:
|
||||
<Bishop IsPromoted="@IsPromoted" />
|
||||
break;
|
||||
case WhichPiece.GoldGeneral:
|
||||
<GoldGeneral />
|
||||
break;
|
||||
case WhichPiece.King:
|
||||
<ChallengingKing />
|
||||
break;
|
||||
case WhichPiece.Knight:
|
||||
<Knight IsPromoted="@IsPromoted" />
|
||||
break;
|
||||
case WhichPiece.Lance:
|
||||
<Lance IsPromoted="@IsPromoted" />
|
||||
break;
|
||||
case WhichPiece.Pawn:
|
||||
<Pawn IsPromoted="@IsPromoted" />
|
||||
break;
|
||||
case WhichPiece.Rook:
|
||||
<Rook IsPromoted="@IsPromoted" />
|
||||
break;
|
||||
case WhichPiece.SilverGeneral:
|
||||
<SilverGeneral IsPromoted="@IsPromoted" />
|
||||
break;
|
||||
default:
|
||||
@*render nothing*@
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public Contracts.Types.Piece? Piece { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public WhichPlayer Perspective { get; set; }
|
||||
|
||||
private bool IsPromoted => Piece != null && Piece.IsPromoted;
|
||||
}
|
||||
8
Shogi.UI/Pages/Home/GamePiece.razor.css
Normal file
8
Shogi.UI/Pages/Home/GamePiece.razor.css
Normal file
@@ -0,0 +1,8 @@
|
||||
::deep svg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
[data-upsidedown] {
|
||||
transform: rotateZ(180deg);
|
||||
}
|
||||
46
Shogi.UI/Pages/Home/Home.razor
Normal file
46
Shogi.UI/Pages/Home/Home.razor
Normal file
@@ -0,0 +1,46 @@
|
||||
@page "/"
|
||||
@using Shogi.Contracts.Types
|
||||
@using System.Net.WebSockets
|
||||
@using System.Text
|
||||
@inject ModalService modalService
|
||||
@inject AccountManager AccountManager
|
||||
@inject AccountState Account
|
||||
|
||||
@*<Modals />*@
|
||||
|
||||
<main class="shogi">
|
||||
@if (welcomeModalIsVisible)
|
||||
{
|
||||
<LoginModal />
|
||||
}
|
||||
<PageHeader />
|
||||
<GameBrowser ActiveSessionChanged="OnChangeSession" />
|
||||
<GameBoard SessionName="@activeSessionName" />
|
||||
</main>
|
||||
|
||||
@code {
|
||||
bool welcomeModalIsVisible = false;
|
||||
string activeSessionName = string.Empty;
|
||||
ClientWebSocket socket = new ClientWebSocket();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Account.LoginChangedEvent += OnLoginChanged;
|
||||
var success = await AccountManager.TryLoginSilentAsync();
|
||||
if (!success)
|
||||
{
|
||||
welcomeModalIsVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoginChanged(object? sender, LoginEventArgs args)
|
||||
{
|
||||
welcomeModalIsVisible = args.User == null;
|
||||
StateHasChanged();
|
||||
}
|
||||
private void OnChangeSession(SessionMetadata s)
|
||||
{
|
||||
activeSessionName = s.Name;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
30
Shogi.UI/Pages/Home/Home.razor.css
Normal file
30
Shogi.UI/Pages/Home/Home.razor.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.shogi {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"pageHeader board"
|
||||
"browser board";
|
||||
grid-template-columns: minmax(25rem, 25vw) 1fr;
|
||||
grid-template-rows: max-content 1fr;
|
||||
place-items: stretch;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem;
|
||||
position: relative; /* For absolute positioned children. */
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.shogi > ::deep .game-board {
|
||||
grid-area: board;
|
||||
place-self: center;
|
||||
}
|
||||
|
||||
.shogi > ::deep .pageHeader {
|
||||
grid-area: pageHeader;
|
||||
}
|
||||
|
||||
.shogi > ::deep .game-browser {
|
||||
grid-area: browser;
|
||||
}
|
||||
|
||||
Modals {
|
||||
display: none;
|
||||
}
|
||||
56
Shogi.UI/Pages/Home/LoginModal.razor
Normal file
56
Shogi.UI/Pages/Home/LoginModal.razor
Normal file
@@ -0,0 +1,56 @@
|
||||
@inject AccountManager Account
|
||||
|
||||
|
||||
<div class="my-modal-background">
|
||||
<div class="my-modal">
|
||||
@if (guestAccountDescriptionIsVisible)
|
||||
{
|
||||
<h1>What's the difference?</h1>
|
||||
@*<div class="account-description mb-4 bg-light p-2">
|
||||
<h4>Feature</h4>
|
||||
<h4>Guest Accounts</h4>
|
||||
<h4>Email Accounts</h4>
|
||||
|
||||
<div>Resume in-progress games from any browser on any device.</div>
|
||||
<span class="oi oi-circle-x" title="circle-x" aria-hidden="true"></span>
|
||||
<span class="oi oi-circle-check" title="circle-check" aria-hidden="true"></span>
|
||||
</div>*@
|
||||
<p>
|
||||
Guest accounts are session based, meaning that the account lives exclusively within the device and browser you create the account on.
|
||||
This is the only difference between guest and email accounts.
|
||||
</p>
|
||||
<div class="alert alert-warning">
|
||||
Deleting your device's browser storage for this site also deletes your guest account. This data is how you are remembered between sessions.
|
||||
</div>
|
||||
<button class="btn btn-link smaller" @onclick="HideGuestAccountDescription">Take me back</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
<h1>Welcome to Shogi!</h1>
|
||||
<div>
|
||||
<p>How would you like to proceed?</p>
|
||||
<p>
|
||||
<button @onclick="Account.LoginWithMicrosoftAccount">Log in</button>
|
||||
<button @onclick="Account.LoginWithGuestAccount">Proceed as Guest</button>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<button class="btn btn-link smaller" @onclick="ShowGuestAccountDescription">What's the difference?</button>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
bool guestAccountDescriptionIsVisible = false;
|
||||
|
||||
void ShowGuestAccountDescription()
|
||||
{
|
||||
guestAccountDescriptionIsVisible = true;
|
||||
}
|
||||
void HideGuestAccountDescription()
|
||||
{
|
||||
guestAccountDescriptionIsVisible = false;
|
||||
}
|
||||
}
|
||||
21
Shogi.UI/Pages/Home/LoginModal.razor.css
Normal file
21
Shogi.UI/Pages/Home/LoginModal.razor.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.my-modal-background {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: fixed;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
inset: 0;
|
||||
z-index: 900;
|
||||
}
|
||||
|
||||
.my-modal {
|
||||
text-align: center;
|
||||
background-color: var(--contrast-color);
|
||||
padding: 1rem;
|
||||
max-width: 40rem;
|
||||
}
|
||||
|
||||
.account-description {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content max-content;
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
31
Shogi.UI/Pages/Home/PageHeader.razor
Normal file
31
Shogi.UI/Pages/Home/PageHeader.razor
Normal file
@@ -0,0 +1,31 @@
|
||||
@inject AccountState Account
|
||||
@inject AccountManager AccountManager
|
||||
|
||||
<div class="pageHeader">
|
||||
<h1>Shogi</h1>
|
||||
@if (user != null)
|
||||
{
|
||||
<div class="user">
|
||||
<div>@user.DisplayName</div>
|
||||
<button type="button" class="logout" @onclick="AccountManager.LogoutAsync">Logout</button>
|
||||
</div>
|
||||
}
|
||||
@*<LoginDisplay />*@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private User? user;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Account.LoginChangedEvent += OnLoginChange;
|
||||
}
|
||||
|
||||
private void OnLoginChange(object? sender, LoginEventArgs args)
|
||||
{
|
||||
if (args == null)
|
||||
throw new ArgumentException(nameof(args));
|
||||
user = args.User;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
20
Shogi.UI/Pages/Home/PageHeader.razor.css
Normal file
20
Shogi.UI/Pages/Home/PageHeader.razor.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.pageHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
place-items: center stretch;
|
||||
place-content: stretch;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--contrast-color);
|
||||
}
|
||||
|
||||
.pageHeader h1 {
|
||||
place-self: baseline start;
|
||||
}
|
||||
|
||||
.pageHeader button.logout {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.pageHeader .user {
|
||||
text-align: right;
|
||||
}
|
||||
32
Shogi.UI/Pages/Home/Pieces/Bishop.razor
Normal file
32
Shogi.UI/Pages/Home/Pieces/Bishop.razor
Normal file
File diff suppressed because one or more lines are too long
10
Shogi.UI/Pages/Home/Pieces/ChallengingKing.razor
Normal file
10
Shogi.UI/Pages/Home/Pieces/ChallengingKing.razor
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
10
Shogi.UI/Pages/Home/Pieces/GoldGeneral.razor
Normal file
10
Shogi.UI/Pages/Home/Pieces/GoldGeneral.razor
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
31
Shogi.UI/Pages/Home/Pieces/Knight.razor
Normal file
31
Shogi.UI/Pages/Home/Pieces/Knight.razor
Normal file
File diff suppressed because one or more lines are too long
31
Shogi.UI/Pages/Home/Pieces/Lance.razor
Normal file
31
Shogi.UI/Pages/Home/Pieces/Lance.razor
Normal file
File diff suppressed because one or more lines are too long
31
Shogi.UI/Pages/Home/Pieces/Pawn.razor
Normal file
31
Shogi.UI/Pages/Home/Pieces/Pawn.razor
Normal file
File diff suppressed because one or more lines are too long
10
Shogi.UI/Pages/Home/Pieces/ReigningKing.razor
Normal file
10
Shogi.UI/Pages/Home/Pieces/ReigningKing.razor
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
31
Shogi.UI/Pages/Home/Pieces/Rook.razor
Normal file
31
Shogi.UI/Pages/Home/Pieces/Rook.razor
Normal file
File diff suppressed because one or more lines are too long
31
Shogi.UI/Pages/Home/Pieces/SilverGeneral.razor
Normal file
31
Shogi.UI/Pages/Home/Pieces/SilverGeneral.razor
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user