Refactor challenge creation and joining logic
The challenge creation and accepting logic has been refactored into a separate ChallengesController. This includes the addition of several new views, changes to existing ones, and backend logic improvements. Irrelevant 'Home' controller codes have been removed. Also, better error handling has been added to the 'PlayersController'.
This commit is contained in:
@@ -17,4 +17,17 @@ public class ValidNickName : ValidationAttribute
|
||||
{
|
||||
return this.IsValid(value) ? ValidationResult.Success : new ValidationResult("The " + validationContext.DisplayName + " can only contain numbers, alphabet and underline.");
|
||||
}
|
||||
}
|
||||
|
||||
public class IsGuid : ValidationAttribute
|
||||
{
|
||||
public override bool IsValid(object? value)
|
||||
{
|
||||
return value is string input && Guid.TryParse(input, out _);
|
||||
}
|
||||
|
||||
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
|
||||
{
|
||||
return this.IsValid(value) ? ValidationResult.Success : new ValidationResult("The " + validationContext.DisplayName + " is not a valid GUID.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Aiursoft.AiurObserver.Extensions;
|
||||
using Aiursoft.AiurObserver.WebSocket.Server;
|
||||
using Aiursoft.ChessServer.Attributes;
|
||||
using Aiursoft.ChessServer.Data;
|
||||
using Aiursoft.ChessServer.Models;
|
||||
using Aiursoft.ChessServer.Models.ViewModels;
|
||||
using Aiursoft.CSTools.Services;
|
||||
using Aiursoft.WebTools.Attributes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Aiursoft.ChessServer.Controllers;
|
||||
|
||||
[Route("challenge")]
|
||||
public class ChallengesController (
|
||||
Counter counter,
|
||||
InMemoryDatabase database) : Controller
|
||||
{
|
||||
[Route("")]
|
||||
[HttpGet]
|
||||
public IActionResult Auto(Guid playerId)
|
||||
{
|
||||
// I created a challenge. Go to my challenge.
|
||||
var myChallengeKey = database.GetMyOpenChallenge(playerId);
|
||||
if (myChallengeKey != null)
|
||||
{
|
||||
return RedirectToAction(nameof(Challenge), new { id = (int)myChallengeKey });
|
||||
}
|
||||
|
||||
// Exists a public challenge. Go to that challenge.
|
||||
var otherChallenge = database.GetFirstPublicChallengeKey();
|
||||
if (otherChallenge != null)
|
||||
{
|
||||
return RedirectToAction(nameof(Challenge), new { id = (int)otherChallenge });
|
||||
}
|
||||
|
||||
// Create a new challenge.
|
||||
return RedirectToAction(nameof(Create), new { playerId });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
public IActionResult Create(Guid playerId)
|
||||
{
|
||||
var myChallengeKey = database.GetMyOpenChallenge(playerId);
|
||||
if (myChallengeKey != null)
|
||||
{
|
||||
// This player already has a challenge. Redirect to that challenge.
|
||||
return RedirectToAction(nameof(Challenge), new { id = myChallengeKey, playerId });
|
||||
}
|
||||
var model = new CreateChallengeViewModel();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("create")]
|
||||
public IActionResult Create(CreateChallengeViewModel model)
|
||||
{
|
||||
// Ensure single challenge can be created by a player.
|
||||
var myChallengeKey = database.GetMyOpenChallenge(model.CreatorId);
|
||||
if (myChallengeKey != null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CreatorId), "You already have a challenge!");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// Create a new challenge.
|
||||
var player = database.GetOrAddPlayer(model.CreatorId);
|
||||
var challenge = new Challenge(
|
||||
creator: player,
|
||||
message: model.Message,
|
||||
roleRule: model.RoleRule,
|
||||
timeLimit: model.TimeLimit,
|
||||
permission: model.Permission);
|
||||
var challengeId = counter.GetUniqueNo();
|
||||
database.CreateChallenge(challengeId, challenge);
|
||||
return RedirectToAction(nameof(Challenge), new { id = challengeId });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id:int}")]
|
||||
public IActionResult Challenge(int id)
|
||||
{
|
||||
var challenge = database.GetChallenge(id);
|
||||
if (challenge == null)
|
||||
{
|
||||
// Challenge not found.
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (challenge is AcceptedChallenge)
|
||||
{
|
||||
// Challenge already accepted.
|
||||
return RedirectToAction(nameof(GamesController.GetHtml), "Games", new { id });
|
||||
}
|
||||
|
||||
var model = new ChallengeViewModel
|
||||
{
|
||||
ChallengeId = id,
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{id:int}/drop")]
|
||||
public IActionResult Drop([FromRoute][Required]int id, [FromForm][Required][IsGuid]string playerId)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return RedirectToAction(nameof(Challenge), new { id });
|
||||
}
|
||||
var challenge = database.GetChallenge(id);
|
||||
if (challenge != null && challenge.Creator.Id == Guid.Parse(playerId))
|
||||
{
|
||||
database.DeleteChallenge(id);
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index), "Home");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("accept/{id:int}")]
|
||||
public async Task<IActionResult> Accept([FromRoute]int id, [FromQuery]Guid playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await database.PatchChallengeAsAcceptedAsync(id, playerId);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[EnforceWebSocket]
|
||||
[Route("listen/{id:int}.ws")]
|
||||
public async Task ListenChallenge(int id)
|
||||
{
|
||||
var pusher = await HttpContext.AcceptWebSocketClient();
|
||||
var challenge = database.GetChallenge(id);
|
||||
if (challenge == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var outSub = challenge
|
||||
.ChallengeChangedChannel
|
||||
.Subscribe(t => pusher.Send(t, HttpContext.RequestAborted));
|
||||
try
|
||||
{
|
||||
await pusher.Listen(HttpContext.RequestAborted);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Ignore. This happens when the client closes the connection.
|
||||
}
|
||||
finally
|
||||
{
|
||||
await pusher.Close(HttpContext.RequestAborted);
|
||||
outSub.Unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Aiursoft.ChessServer.Data;
|
||||
using Aiursoft.ChessServer.Models.ViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Aiursoft.AiurObserver.Extensions;
|
||||
using Aiursoft.ChessServer.Attributes;
|
||||
using Aiursoft.WebTools.Attributes;
|
||||
|
||||
namespace Aiursoft.ChessServer.Controllers;
|
||||
@@ -24,13 +25,14 @@ public class GamesController(InMemoryDatabase database) : Controller
|
||||
}
|
||||
|
||||
[Route("{id:int}.color")]
|
||||
public IActionResult GetColor([FromRoute] int id, [FromQuery] string playerId)
|
||||
public IActionResult GetColor([FromRoute] int id, [FromQuery][IsGuid] string playerId)
|
||||
{
|
||||
var validId = Guid.TryParse(playerId, out var playerGuid);
|
||||
if (!validId)
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return NotFound();
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
var playerGuid = Guid.Parse(playerId);
|
||||
var challenge = database.GetAcceptedChallenge(id);
|
||||
if (challenge == null)
|
||||
{
|
||||
@@ -42,22 +44,21 @@ public class GamesController(InMemoryDatabase database) : Controller
|
||||
|
||||
[Route("{id:int}.ws")]
|
||||
[EnforceWebSocket]
|
||||
public async Task GetWebSocket([FromRoute] int id, [FromQuery] string playerId)
|
||||
public async Task GetWebSocket([FromRoute] int id, [FromQuery][IsGuid] string playerId)
|
||||
{
|
||||
var validId = Guid.TryParse(playerId, out var playerGuid);
|
||||
if (!validId)
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var playerGuid = Guid.Parse(playerId);
|
||||
var challenge = database.GetAcceptedChallenge(id);
|
||||
if (challenge == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var game = challenge.Game;
|
||||
var pusher = await HttpContext.AcceptWebSocketClient();
|
||||
var outSub = game
|
||||
var outSub = challenge.Game
|
||||
.FenChangedChannel
|
||||
.Subscribe(t => pusher.Send(t, HttpContext.RequestAborted));
|
||||
|
||||
@@ -65,17 +66,17 @@ public class GamesController(InMemoryDatabase database) : Controller
|
||||
.Filter(t => !string.IsNullOrWhiteSpace(t))
|
||||
.Subscribe(async move =>
|
||||
{
|
||||
lock (game.MovePieceLock)
|
||||
lock (challenge.Game.MovePieceLock)
|
||||
{
|
||||
if (!game.Board.IsEndGame &&
|
||||
game.Board.IsValidMove(move) &&
|
||||
if (!challenge.Game.Board.IsEndGame &&
|
||||
challenge.Game.Board.IsValidMove(move) &&
|
||||
challenge.GetTurnPlayer().Id == playerGuid)
|
||||
{
|
||||
game.Board.Move(move);
|
||||
challenge.Game.Board.Move(move);
|
||||
}
|
||||
}
|
||||
|
||||
await game.FenChangedChannel.BroadcastAsync(game.Board.ToFen());
|
||||
await challenge.Game.FenChangedChannel.BroadcastAsync(challenge.Game.Board.ToFen());
|
||||
});
|
||||
|
||||
try
|
||||
|
||||
@@ -9,9 +9,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Aiursoft.ChessServer.Controllers;
|
||||
|
||||
public class HomeController(
|
||||
Counter counter,
|
||||
InMemoryDatabase database) : Controller
|
||||
public class HomeController(InMemoryDatabase database) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult Index()
|
||||
@@ -32,176 +30,4 @@ public class HomeController(
|
||||
};
|
||||
return View(nameof(Index), model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This action will auto redirect to a challenge.
|
||||
///
|
||||
/// If the player has a challenge, then go to that challenge.
|
||||
///
|
||||
/// If not, then go to a public challenge. If not, then create a new challenge.
|
||||
/// </summary>
|
||||
/// <param name="playerId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public IActionResult Auto(Guid playerId)
|
||||
{
|
||||
// I created a challenge. Go to my challenge.
|
||||
var myChallengeKey = database.GetMyOpenChallenge(playerId);
|
||||
if (myChallengeKey != null)
|
||||
{
|
||||
return RedirectToAction(nameof(Challenge), new { id = (int)myChallengeKey });
|
||||
}
|
||||
|
||||
// Exists a public challenge. Go to that challenge.
|
||||
var otherChallenge = database.GetFirstPublicChallengeKey();
|
||||
if (otherChallenge != null)
|
||||
{
|
||||
return RedirectToAction(nameof(Challenge), new { id = (int)otherChallenge });
|
||||
}
|
||||
|
||||
// Create a new challenge.
|
||||
return RedirectToAction(nameof(Create), new { playerId });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This action renders a page to create a new challenge.
|
||||
///
|
||||
/// However, if the player has a challenge, then redirect to that challenge.
|
||||
/// </summary>
|
||||
/// <param name="playerId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public IActionResult Create(Guid playerId)
|
||||
{
|
||||
var myChallengeKey = database.GetMyOpenChallenge(playerId);
|
||||
if (myChallengeKey != null)
|
||||
{
|
||||
// This player already has a challenge. Redirect to that challenge.
|
||||
return RedirectToAction(nameof(Challenge), new { id = myChallengeKey, playerId });
|
||||
}
|
||||
var model = new CreateChallengeViewModel();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult Create(CreateChallengeViewModel model)
|
||||
{
|
||||
// Ensure single challenge can be created by a player.
|
||||
var myChallengeKey = database.GetMyOpenChallenge(model.CreatorId);
|
||||
if (myChallengeKey != null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CreatorId), "You already have a challenge!");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// Create a new challenge.
|
||||
var player = database.GetOrAddPlayer(model.CreatorId);
|
||||
var challenge = new Challenge(
|
||||
creator: player,
|
||||
message: model.Message,
|
||||
roleRule: model.RoleRule,
|
||||
timeLimit: model.TimeLimit,
|
||||
permission: model.Permission);
|
||||
var challengeId = counter.GetUniqueNo();
|
||||
database.CreateChallenge(challengeId, challenge);
|
||||
return RedirectToAction(nameof(Challenge), new { id = challengeId });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This action renders a page to show the details of a challenge.
|
||||
///
|
||||
/// Will use JavaScript to call accept challenge API.
|
||||
///
|
||||
/// Will use WebSocket to listen to the challenge changes.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public IActionResult Challenge(int id)
|
||||
{
|
||||
var challenge = database.GetChallenge(id);
|
||||
if (challenge == null)
|
||||
{
|
||||
// Challenge not found.
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (challenge is AcceptedChallenge)
|
||||
{
|
||||
// Challenge already accepted.
|
||||
return RedirectToAction(nameof(GamesController.GetHtml), "Games", new { id });
|
||||
}
|
||||
|
||||
var model = new ChallengeViewModel
|
||||
{
|
||||
ChallengeId = id,
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult DropChallenge(DropChallengeViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return RedirectToAction(nameof(Challenge), new { id = model.Id, playerId = model.PlayerId });
|
||||
}
|
||||
var challenge = database.GetChallenge(model.Id);
|
||||
if (challenge != null && challenge.Creator.Id == model.PlayerId)
|
||||
{
|
||||
database.DeleteChallenge(model.Id);
|
||||
}
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method accepts a challenge and updates the challenge's accepter.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the challenge to accept.</param>
|
||||
/// <param name="playerId">The ID of the player accepting the challenge.</param>
|
||||
/// <returns>An IActionResult indicating the result of the operation.</returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AcceptChallenge([FromRoute]int id, [FromQuery]Guid playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await database.PatchChallengeAsAcceptedAsync(id, playerId);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Route("listen-challenge/{id:int}.ws")]
|
||||
[EnforceWebSocket]
|
||||
public async Task ListenChallenge(int id)
|
||||
{
|
||||
var pusher = await HttpContext.AcceptWebSocketClient();
|
||||
var challenge = database.GetChallenge(id);
|
||||
if (challenge == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var outSub = challenge
|
||||
.ChallengeChangedChannel
|
||||
.Subscribe(t => pusher.Send(t, HttpContext.RequestAborted));
|
||||
try
|
||||
{
|
||||
await pusher.Listen(HttpContext.RequestAborted);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Ignore. This happens when the client closes the connection.
|
||||
}
|
||||
finally
|
||||
{
|
||||
await pusher.Close(HttpContext.RequestAborted);
|
||||
outSub.Unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,11 @@ public class PlayersController(InMemoryDatabase database) : ControllerBase
|
||||
|
||||
[HttpPut]
|
||||
[Route("{id:guid}/new-name/{nickname}")]
|
||||
public IActionResult ChangeNickname([Required]Guid id, [Required][MaxLength(20)][ValidNickName]string nickname)
|
||||
public IActionResult ChangeNickname([Required]Guid id, [Required][MaxLength(40)][ValidNickName]string nickname)
|
||||
{
|
||||
if (ModelState.IsValid == false)
|
||||
{
|
||||
return BadRequest();
|
||||
return BadRequest("Only numbers, alphabet and underline are allowed in nickname. Max length is 40.");
|
||||
}
|
||||
|
||||
var me = database.GetOrAddPlayer(id);
|
||||
|
||||
+6
-9
@@ -1,14 +1,13 @@
|
||||
@using Aiursoft.ChessServer.Controllers
|
||||
@model Aiursoft.ChessServer.Models.ViewModels.ChallengeViewModel
|
||||
@model Aiursoft.ChessServer.Models.ViewModels.ChallengeViewModel
|
||||
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1 class="display-4">Waiting for opponent joining...</h1>
|
||||
<p class="lead">Please share the link of this room to your friend!</p>
|
||||
@{
|
||||
var link = $"{Context.Request.Scheme}://{Context.Request.Host}/Home/{nameof(HomeController.Challenge)}/{Model.ChallengeId}";
|
||||
var link = $"{Context.Request.Scheme}://{Context.Request.Host}/challenge/{Model.ChallengeId}";
|
||||
}
|
||||
<form asp-controller="Home" asp-action="DropChallenge" asp-route-id="@Model.ChallengeId" method="post" class="d-inline" asp-antiforgery="false">
|
||||
<form asp-controller="Challenges" asp-action="Drop" asp-route-id="@Model.ChallengeId" method="post" class="d-inline" asp-antiforgery="false">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<input type="hidden" name="playerId" value=""/>
|
||||
|
||||
@@ -48,13 +47,11 @@
|
||||
// Listen to the channel
|
||||
const wsScheme = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||
const socket = new WebSocket(
|
||||
`${wsScheme}${window.location.host}/listen-challenge/${@Model.ChallengeId}.ws`
|
||||
`${wsScheme}${window.location.host}/challenge/listen/${@Model.ChallengeId}.ws`
|
||||
);
|
||||
|
||||
socket.addEventListener('open', function (event) {
|
||||
console.log('WebSocket is open now.');
|
||||
|
||||
// Accept the challenge
|
||||
socket.addEventListener('open', function () {
|
||||
console.log('WebSocket is open now. Accepting challenge...');
|
||||
acceptChallenge(@Model.ChallengeId);
|
||||
});
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<div class="card mb-2 col-sm-12 px-1">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Create a new room</h5>
|
||||
<form asp-controller="Home" asp-action="Create" method="post" asp-antiforgery="false">
|
||||
<form asp-controller="Challenges" asp-action="Create" method="post" asp-antiforgery="false">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<input type="hidden" asp-for="CreatorId"/>
|
||||
<div class="form-group">
|
||||
@@ -1,10 +0,0 @@
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
<script type="module">
|
||||
import { getUserId } from "/scripts/player.js";
|
||||
const playerId = getUserId();
|
||||
if (playerId) {
|
||||
window.location.href = window.location.href.toLowerCase().replace("challengenoid", "Challenge") + `?playerId=${playerId}`;
|
||||
}
|
||||
</script>
|
||||
@@ -7,9 +7,9 @@
|
||||
<p class="lead">Join a room, or create a room!</p>
|
||||
<p>
|
||||
@* JS Will Append the player id to the link. *@
|
||||
<a class="btn btn-success btn-lg mt-4" role="button" id="autoButton" asp-controller="Home" asp-action="Auto">Auto join</a>
|
||||
<a class="btn btn-success btn-lg mt-4" role="button" id="autoButton" asp-controller="Challenges" asp-action="Auto">Auto join</a>
|
||||
@* JS Will Append the player id to the link. *@
|
||||
<a class="btn btn-secondary btn-lg mt-4" role="button" id="createButton" asp-controller="Home" asp-action="Create">Create a new room</a>
|
||||
<a class="btn btn-secondary btn-lg mt-4" role="button" id="createButton" asp-controller="Challenges" asp-action="Create">Create a new room</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
<td>@item.Value.Creator.NickName</td>
|
||||
<td>@((item.Value as AcceptedChallenge)?.Accepter.NickName)
|
||||
<td>
|
||||
<a class="btn btn-sm btn-primary" asp-controller="Home" asp-action="Challenge" asp-route-id="@item.Key">Join</a>
|
||||
<a class="btn btn-sm btn-primary" asp-controller="Challenges" asp-action="Challenge" asp-route-id="@item.Key">Join</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ const getUserName = async function () {
|
||||
}
|
||||
|
||||
const changeName = async function (newName) {
|
||||
// call /players/{id}/{new-name} HTTP PUT API:
|
||||
await fetch(`/players/${getUserId()}/new-name/${newName}`, {method: 'PUT'});
|
||||
const response = await fetch(`/players/${getUserId()}/new-name/${newName}`, {method: 'PUT'});
|
||||
if (!response.ok) {
|
||||
alert(`Failed to change your name. ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
const getAcceptedChallenge = async function (challengeId) {
|
||||
@@ -29,7 +31,7 @@ const getAcceptedChallenge = async function (challengeId) {
|
||||
|
||||
const acceptChallenge = async function (challengeId) {
|
||||
const playerId = getUserId();
|
||||
await fetch(`/home/AcceptChallenge/${challengeId}?playerId=${playerId}`, {method: 'POST'});
|
||||
await fetch(`/challenge/accept/${challengeId}?playerId=${playerId}`, {method: 'POST'});
|
||||
}
|
||||
|
||||
const getPlayerColor = async function (challengeId) {
|
||||
|
||||
Reference in New Issue
Block a user