diff --git a/src/Aiursoft.ChessServer/Attributes/ValidNickName.cs b/src/Aiursoft.ChessServer/Attributes/ValidNickName.cs new file mode 100644 index 0000000..8f5c182 --- /dev/null +++ b/src/Aiursoft.ChessServer/Attributes/ValidNickName.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; + +namespace Aiursoft.ChessServer.Attributes; + +public class ValidNickName : ValidationAttribute +{ + private readonly string _keyWordRegex = "^[-a-zA-Z0-9_]+$"; + + public override bool IsValid(object? value) + { + var regex = new Regex(_keyWordRegex, RegexOptions.Compiled); + return value is string input && regex.IsMatch(input); + } + + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + return this.IsValid(value) ? ValidationResult.Success : new ValidationResult("The " + validationContext.DisplayName + " can only contain numbers, alphabet and underline."); + } +} \ No newline at end of file diff --git a/src/Aiursoft.ChessServer/Controllers/HomeController.cs b/src/Aiursoft.ChessServer/Controllers/HomeController.cs index 6d4a812..a8eaa6f 100644 --- a/src/Aiursoft.ChessServer/Controllers/HomeController.cs +++ b/src/Aiursoft.ChessServer/Controllers/HomeController.cs @@ -18,18 +18,41 @@ public class HomeController : Controller _database = database; } + [HttpGet] public IActionResult Index() { var model = new IndexViewModel(_database.Challenges); return View(model); } + + [HttpGet] + public IActionResult Auto(Guid playerId) + { + // I created a challenge. Go to my challenge. + var iHaveAChallenge = _database.Challenges.FirstOrDefault(t => t.Value.Creator.Id == playerId); + if (iHaveAChallenge.Value != null) + { + return RedirectToAction(nameof(Challenge), new { id = iHaveAChallenge.Key, playerId }); + } + + // Exists a public challenge. Go to that challenge. + var otherChallenge = _database.Challenges.FirstOrDefault(t => t.Value.Permission == ChallengePermission.Public); + if (otherChallenge.Value != null) + { + return RedirectToAction(nameof(Challenge), new { id = otherChallenge.Key, playerId }); + } + + // Create a new challenge. + return RedirectToAction(nameof(Create), new { playerId }); + } + [HttpGet] public IActionResult Create(Guid playerId) { var iHaveAChallenge = _database.Challenges.FirstOrDefault(t => t.Value.Creator.Id == playerId); if (iHaveAChallenge.Value != null) { - return RedirectToAction(nameof(Room), new { id = iHaveAChallenge.Key }); + return RedirectToAction(nameof(Challenge), new { id = iHaveAChallenge.Key, playerId }); } var model = new CreateChallengeViewModel(); return View(model); @@ -38,6 +61,11 @@ public class HomeController : Controller [HttpPost] public IActionResult Create(CreateChallengeViewModel model) { + if (!ModelState.IsValid) + { + return View(model); + } + var player = _database.GetOrAddPlayer(model.CreatorId); var challenge = new Challenge(player) { @@ -46,14 +74,38 @@ public class HomeController : Controller Permission = model.Permission, TimeLimit = model.TimeLimit, }; - var uniqueId = _counter.GetUniqueNo(); - _database.Challenges.TryAdd(uniqueId, challenge); - return RedirectToAction(nameof(Room), new { id = uniqueId }); + var roomId = _counter.GetUniqueNo(); + _database.Challenges.TryAdd(roomId, challenge); + return RedirectToAction(nameof(Challenge), new { id = roomId, playerId = model.CreatorId }); } - public IActionResult Room(int id) + [HttpGet] + public IActionResult Challenge(int id, Guid playerId) { - // Not implemented. - return Ok(); + var player = _database.GetOrAddPlayer(playerId); + var challenge = _database.GetOrAddChallenge(id, player); + var model = new ChallengeViewModel() + { + RoomId = id, + PlayerId = playerId, + IsCreator = challenge.Creator.Id == playerId, + }; + return View(model); + } + + [HttpPost] + public IActionResult DropChallenge(DropChallengeViewModel model) + { + if (!ModelState.IsValid) + { + return RedirectToAction(nameof(Challenge), new { id = model.Id, playerId = model.PlayerId }); + } + var player = _database.GetOrAddPlayer(model.PlayerId); + var challenge = _database.GetOrAddChallenge(model.Id, player); + if (challenge.Creator.Id == model.PlayerId) + { + _database.Challenges.TryRemove(model.Id, out _); + } + return RedirectToAction(nameof(Index)); } } \ No newline at end of file diff --git a/src/Aiursoft.ChessServer/Controllers/PlayersController.cs b/src/Aiursoft.ChessServer/Controllers/PlayersController.cs index 86816d6..0e97b00 100644 --- a/src/Aiursoft.ChessServer/Controllers/PlayersController.cs +++ b/src/Aiursoft.ChessServer/Controllers/PlayersController.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; +using Aiursoft.ChessServer.Attributes; using Aiursoft.ChessServer.Data; -using Aiursoft.CSTools.Attributes; using Microsoft.AspNetCore.Mvc; namespace Aiursoft.ChessServer.Controllers; @@ -24,7 +24,7 @@ public class PlayersController : ControllerBase [HttpPut] [Route("{id:guid}/new-name/{nickname}")] - public IActionResult ChangeNickname([Required]Guid id, [Required][MaxLength(20)][ValidDomainName]string nickname) + public IActionResult ChangeNickname([Required]Guid id, [Required][MaxLength(20)][ValidNickName]string nickname) { if (ModelState.IsValid == false) { diff --git a/src/Aiursoft.ChessServer/Data/InMemoryDatabase.cs b/src/Aiursoft.ChessServer/Data/InMemoryDatabase.cs index 44a4ec4..6b6df3f 100644 --- a/src/Aiursoft.ChessServer/Data/InMemoryDatabase.cs +++ b/src/Aiursoft.ChessServer/Data/InMemoryDatabase.cs @@ -35,4 +35,12 @@ public class InMemoryDatabase : ISingletonDependency }); } } + + public Challenge GetOrAddChallenge(int id, Player creator) + { + lock (Challenges) + { + return Challenges.GetOrAdd(id, _ => new Challenge(creator)); + } + } } \ No newline at end of file diff --git a/src/Aiursoft.ChessServer/Models/ViewModels/ChallengeViewModel.cs b/src/Aiursoft.ChessServer/Models/ViewModels/ChallengeViewModel.cs new file mode 100644 index 0000000..891c89b --- /dev/null +++ b/src/Aiursoft.ChessServer/Models/ViewModels/ChallengeViewModel.cs @@ -0,0 +1,10 @@ +namespace Aiursoft.ChessServer.Models; + +public class ChallengeViewModel +{ + public int RoomId { get; set; } + + public Guid PlayerId { get; set; } + + public bool IsCreator { get; set; } +} \ No newline at end of file diff --git a/src/Aiursoft.ChessServer/Models/ViewModels/CreateChallengeViewModel.cs b/src/Aiursoft.ChessServer/Models/ViewModels/CreateChallengeViewModel.cs index ba9af36..ac07771 100644 --- a/src/Aiursoft.ChessServer/Models/ViewModels/CreateChallengeViewModel.cs +++ b/src/Aiursoft.ChessServer/Models/ViewModels/CreateChallengeViewModel.cs @@ -1,14 +1,23 @@ -namespace Aiursoft.ChessServer.Models; +using System.ComponentModel.DataAnnotations; + +namespace Aiursoft.ChessServer.Models; public class CreateChallengeViewModel { - public Guid CreatorId { get; set; } + [Required] + public Guid CreatorId { get; init; } - public string Message { get; set; } = "A chess room."; + [Required] + [MinLength(3)] + [MaxLength(20)] + public string Message { get; init; } = "A chess room."; - public RoleRule RoleRule { get; set; } = RoleRule.Random; + [Required] + public RoleRule RoleRule { get; init; } = RoleRule.Random; - public TimeSpan TimeLimit { get; set; } = TimeSpan.FromMinutes(10); + [Required] + public TimeSpan TimeLimit { get; init; } = TimeSpan.FromMinutes(10); - public ChallengePermission Permission { get; set; } = ChallengePermission.Public; + [Required] + public ChallengePermission Permission { get; init; } = ChallengePermission.Public; } \ No newline at end of file diff --git a/src/Aiursoft.ChessServer/Models/ViewModels/LeaveRoomViewModel.cs b/src/Aiursoft.ChessServer/Models/ViewModels/LeaveRoomViewModel.cs new file mode 100644 index 0000000..d1bad1f --- /dev/null +++ b/src/Aiursoft.ChessServer/Models/ViewModels/LeaveRoomViewModel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; + +namespace Aiursoft.ChessServer.Models; + +public class DropChallengeViewModel +{ + [FromRoute] + [Required] + public int Id { get; set; } + + [FromForm] + [Required] + public Guid PlayerId { get; set; } +} \ No newline at end of file diff --git a/src/Aiursoft.ChessServer/Views/Home/Challenge.cshtml b/src/Aiursoft.ChessServer/Views/Home/Challenge.cshtml new file mode 100644 index 0000000..551e8d3 --- /dev/null +++ b/src/Aiursoft.ChessServer/Views/Home/Challenge.cshtml @@ -0,0 +1,38 @@ +@model Aiursoft.ChessServer.Models.ChallengeViewModel + +
+
+

Waiting for joining...

+

Please share the link of this room to your friend!

+ @{ + var link = $"{Context.Request.Scheme}://{Context.Request.Host}/Home/Room/{Model.RoomId}"; + } +
+
+ + + + + Copy link + + +
+
+
+ +@section scripts +{ + + +} \ No newline at end of file diff --git a/src/Aiursoft.ChessServer/Views/Home/Create.cshtml b/src/Aiursoft.ChessServer/Views/Home/Create.cshtml index 73ca529..bc40746 100644 --- a/src/Aiursoft.ChessServer/Views/Home/Create.cshtml +++ b/src/Aiursoft.ChessServer/Views/Home/Create.cshtml @@ -15,7 +15,8 @@
Create a new room
-
+ +
diff --git a/src/Aiursoft.ChessServer/Views/Home/Index.cshtml b/src/Aiursoft.ChessServer/Views/Home/Index.cshtml index 8be97e4..871f903 100644 --- a/src/Aiursoft.ChessServer/Views/Home/Index.cshtml +++ b/src/Aiursoft.ChessServer/Views/Home/Index.cshtml @@ -5,7 +5,7 @@

Welcome challenger!

Join a room, or create a room!

- + Auto join Create a new room

@@ -47,11 +47,13 @@ } diff --git a/src/Aiursoft.ChessServer/Views/Shared/_Layout.cshtml b/src/Aiursoft.ChessServer/Views/Shared/_Layout.cshtml index 9d829eb..db1616b 100644 --- a/src/Aiursoft.ChessServer/Views/Shared/_Layout.cshtml +++ b/src/Aiursoft.ChessServer/Views/Shared/_Layout.cshtml @@ -59,11 +59,12 @@
- - - - - + + + + + + @(await RenderSectionAsync("scripts", false)) diff --git a/src/Aiursoft.ChessServer/wwwroot/package.json b/src/Aiursoft.ChessServer/wwwroot/package.json index c8a6a86..eaf89c8 100644 --- a/src/Aiursoft.ChessServer/wwwroot/package.json +++ b/src/Aiursoft.ChessServer/wwwroot/package.json @@ -8,7 +8,8 @@ "@aiursoft/autodark.js": "^1.2.0", "@chrisoakman/chessboardjs": "^1.0.0", "chess.js": "^1.0.0-beta.6", + "clipboard": "^2.0.11", "jquery-validation": "^1.19.5", "jquery-validation-unobtrusive": "^3.2.12" } -} \ No newline at end of file +}