Create game from form #15

Merged
Egamorf merged 8 commits from feature/create-game into main 2025-04-15 23:33:02 +02:00
36 changed files with 383 additions and 114 deletions

View File

@@ -0,0 +1,12 @@
using GameIdeas.Shared.Dto;
namespace GameIdeas.BlazorApp.Helpers;
public static class GameHelper
{
public static void WriteTrackingDto(GameDto game)
{
game.CreationUserId = 100000;
game.CreationDate = DateTime.Now;
}
}

View File

@@ -1,31 +1,36 @@
@using GameIdeas.BlazorApp.Shared.Components.SelectSearch @using Blazored.FluentValidation
@using GameIdeas.BlazorApp.Shared.Components.CircleLoader
@using GameIdeas.BlazorApp.Shared.Components.SelectSearch
@using GameIdeas.BlazorApp.Shared.Components.Slider @using GameIdeas.BlazorApp.Shared.Components.Slider
@using GameIdeas.Shared.Dto @using GameIdeas.Shared.Dto
<EditForm EditContext="EditContext" OnSubmit="HandleOnSubmit"> <EditForm EditContext="EditContext" OnSubmit="HandleOnSubmit">
<FluentValidationValidator/>
<div class="game-form"> <div class="game-form">
<div class="container"> <div class="container">
<div class="input-game"> <div class="input-game">
<div id="first-label" class="label">@ResourcesKey.Title :</div> <div id="first-label" class="label">@ResourcesKey.Title :</div>
<input type="text" class="title" @bind=GameDto.Title> <InputText class="title" @bind-Value=GameDto.Title/>
</div> </div>
<div class="input-game"> <div class="input-game">
<div class="label">@ResourcesKey.ReleaseDate :</div> <div class="label">@ResourcesKey.ReleaseDate :</div>
<input type="date" class="date" @bind=GameDto.ReleaseDate> <InputDate TValue="DateTime?" class="date" @bind-Value=GameDto.ReleaseDate />
</div> </div>
<div class="input-game"> <div class="input-game">
<div class="label">@ResourcesKey.StorageSizeMo :</div> <div class="label">@ResourcesKey.StorageSizeMo :</div>
<input type="number" class="storage" @bind=GameDto.StorageSpace> <InputNumber TValue="double?" class="storage" @bind-Value=GameDto.StorageSpace />
</div> </div>
<div class="input-game"> <div class="input-game">
<div class="label">@ResourcesKey.Developers :</div> <div class="label">@ResourcesKey.Developers :</div>
<SelectSearch TItem="DeveloperDto" Theme="Theme" GetLabel="@(i => i.Name)" <SelectSearch TItem="DeveloperDto" Theme="Theme" GetLabel="@(i => i.Name)" QuickAdd=true
Items="Categories?.Developers" @bind-Values=GameDto.Developers /> Items="Categories?.Developers" @bind-Values=GameDto.Developers
AddItem="@(str => new DeveloperDto() { Name = str })" />
</div> </div>
<div class="input-game"> <div class="input-game">
<div class="label">@ResourcesKey.Publishers :</div> <div class="label">@ResourcesKey.Publishers :</div>
<SelectSearch TItem="PublisherDto" Theme="Theme" GetLabel="@(i => i.Name)" <SelectSearch TItem="PublisherDto" Theme="Theme" GetLabel="@(i => i.Name)" QuickAdd=true
Items="Categories?.Publishers" @bind-Values=GameDto.Publishers /> Items="Categories?.Publishers" @bind-Values=GameDto.Publishers
AddItem="@(str => new PublisherDto() { Name = str })" />
</div> </div>
</div> </div>
<div class="container"> <div class="container">
@@ -37,32 +42,52 @@
</div> </div>
<div class="input-game"> <div class="input-game">
<div class="label">@ResourcesKey.Properties :</div> <div class="label">@ResourcesKey.Properties :</div>
<SelectSearch TItem="PropertyDto" Theme="Theme" GetLabel="@(i => i.Label)" <SelectSearch TItem="PropertyDto" Theme="Theme" GetLabel="@(i => i.Label)" QuickAdd=true
Items="Categories?.Properties" @bind-Values=GameDto.Properties /> Items="Categories?.Properties" @bind-Values=GameDto.Properties
AddItem="@(str => new PropertyDto() { Label = str })" />
</div> </div>
<div class="input-game"> <div class="input-game">
<div class="label">@ResourcesKey.Tags :</div> <div class="label">@ResourcesKey.Tags :</div>
<SelectSearch TItem="TagDto" Theme="Theme" GetLabel="@(i => i.Label)" <SelectSearch TItem="TagDto" Theme="Theme" GetLabel="@(i => i.Label)" QuickAdd=true
Items="Categories?.Tags" @bind-Values=GameDto.Tags /> Items="Categories?.Tags" @bind-Values=GameDto.Tags
AddItem="@(str => new TagDto() { Label = str })" />
</div> </div>
<div class="input-game"> <div class="input-game">
<div class="label">@ResourcesKey.Platforms :</div> <div class="label">@ResourcesKey.Platforms :</div>
<SelectSearch TItem="PlatformDto" Theme="Theme" GetLabel="@(i => i.Label)" <SelectSearch TItem="PlatformDto" Theme="Theme" GetLabel="@(i => i.Label)" QuickAdd=true
Items="Categories?.Platforms" @bind-Values=GameDto.Platforms /> Items="Categories?.Platforms" @bind-Values=GameDto.Platforms
AddItem="@(str => new PlatformDto() { Label = str })" />
</div> </div>
@foreach (var platform in GameDto.Platforms ?? [])
{
<div class="input-game">
<div class="label">@platform.Label :</div>
<InputText class="url" @bind-Value=platform.Url />
</div>
}
</div> </div>
</div> </div>
<div class="description-container"> <div class="description-container">
<div id="label-description">@ResourcesKey.Description :</div> <div id="label-description">@ResourcesKey.Description :</div>
<input type="text" class="description" @bind-value=GameDto.Description> <InputTextArea class="description" @bind-Value=GameDto.Description />
</div> </div>
<div class="buttons"> <div class="bottom-container">
<button type="reset" class="cancel" @onclick=HandleOnCancel> <ValidationSummary class="invalid-content" />
@ResourcesKey.Reset
</button> <div class="buttons">
<button type="submit" class="submit"> <button type="reset" class="cancel" @onclick=HandleOnCancel disabled="@IsLoading">
@ResourcesKey.Save @ResourcesKey.Reset
</button> </button>
<button type="submit" class="submit" disabled="@IsLoading">
@ResourcesKey.Save
</button>
</div>
</div> </div>
</EditForm> </EditForm>
@if (IsLoading)
{
<CircleLoader />
}

View File

@@ -1,3 +1,5 @@
using GameIdeas.BlazorApp.Helpers;
using GameIdeas.BlazorApp.Pages.Games.Gateways;
using GameIdeas.BlazorApp.Shared.Components.Popup; using GameIdeas.BlazorApp.Shared.Components.Popup;
using GameIdeas.BlazorApp.Shared.Components.Select.Models; using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using GameIdeas.BlazorApp.Shared.Components.Slider; using GameIdeas.BlazorApp.Shared.Components.Slider;
@@ -11,29 +13,27 @@ namespace GameIdeas.BlazorApp.Pages.Games.Components;
public partial class GameCreationForm public partial class GameCreationForm
{ {
[Inject] private IJSRuntime Js { get; set; } = default!; [Inject] private IJSRuntime Js { get; set; } = default!;
[Inject] private IGameGateway GameGateway { get; set; } = default!;
[CascadingParameter] private Popup? Popup { get; set; } [CascadingParameter] private Popup? Popup { get; set; }
[Parameter] public CategoriesDto? Categories { get; set; } [Parameter] public CategoriesDto? Categories { get; set; }
[Parameter] public EventCallback OnSubmit { get; set; }
private GameDto GameDto = new(); private GameDto GameDto = new();
private EditContext? EditContext; private EditContext? EditContext;
private readonly SelectTheme Theme = SelectTheme.Creation; private readonly SelectTheme Theme = SelectTheme.Creation;
private readonly SliderParams SliderParams = new() { Gap = 1, Min = 1, Max = 5 }; private readonly SliderParams SliderParams = new() { Gap = 1, Min = 1, Max = 5 };
private bool IsLoading = false;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
EditContext = new(GameDto); EditContext = new(GameDto);
if (Popup != null)
{
Popup.StateChanged += async (_, isOpen) => await HandlePopupStateChanged();
}
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
private async Task HandlePopupStateChanged() protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
await Js.InvokeVoidAsync("resizeGameForm"); await Js.InvokeVoidAsync("resizeGameForm");
} }
private void HandleOnCancel() private void HandleOnCancel()
@@ -48,6 +48,26 @@ public partial class GameCreationForm
return; return;
} }
try
{
IsLoading = true;
GameHelper.WriteTrackingDto(GameDto);
var gameId = await GameGateway.CreateGame(GameDto);
if (gameId != 0)
{
Popup?.Close();
await OnSubmit.InvokeAsync();
}
}
catch (Exception)
{
throw;
}
finally
{
IsLoading = false;
}
} }
} }

View File

@@ -1,6 +1,7 @@
.game-form { .game-form {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between;
gap: 20px; gap: 20px;
} }
@@ -24,7 +25,7 @@
grid-column: 1; grid-column: 1;
} }
input { ::deep input, ::deep textarea {
width: 100%; width: 100%;
background: var(--input-secondary); background: var(--input-secondary);
border: solid 1px var(--input-selected); border: solid 1px var(--input-selected);
@@ -35,10 +36,19 @@ input {
color: var(--white); color: var(--white);
} }
input[type="date"]::-webkit-calendar-picker-indicator { ::deep input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1); filter: invert(1);
cursor: pointer; cursor: pointer;
} }
::deep input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
}
::deep textarea {
resize: vertical;
min-height: 140px;
}
.description-container { .description-container {
margin-top: 8px; margin-top: 8px;
@@ -63,6 +73,48 @@ input {
align-content: center; align-content: center;
} }
input[type="number"]::-webkit-inner-spin-button { .bottom-container {
-webkit-appearance: none; margin-top: 8px;
height: 28px;
display: flex;
flex-direction: row;
align-items: center;
}
::deep .invalid-content {
display: flex;
flex-wrap: wrap;
flex-shrink: 1;
margin: 0;
padding: 0;
list-style: none;
height: auto;
text-wrap: nowrap;
}
::deep .invalid-content li {
margin-right: 8px;
}
.buttons {
margin-left: auto;
height: 100%;
display: flex;
flex-direction: row;
gap: 8px;
}
.buttons button {
border: none;
outline: none;
background: var(--violet);
border-radius: var(--small-radius);
color: var(--white);
font-weight: bold;
padding: 0 10px;
cursor: pointer;
}
.buttons button:hover {
background: var(--violet-selected);
} }

View File

@@ -0,0 +1,18 @@
using FluentValidation;
using GameIdeas.Resources;
using GameIdeas.Shared.Dto;
namespace GameIdeas.BlazorApp.Pages.Games.Components;
public class GameValidation : AbstractValidator<GameDto>
{
public GameValidation()
{
RuleFor(g => g.Title)
.NotEmpty().WithMessage(ResourcesKey.InvalidTitle);
RuleFor(g => g.Interest)
.GreaterThanOrEqualTo(1).WithMessage(ResourcesKey.InvalidInterest)
.LessThanOrEqualTo(5).WithMessage(ResourcesKey.InvalidInterest);
}
}

View File

@@ -36,10 +36,10 @@ public partial class GameFilter
{ {
Headers = SortTypes, Headers = SortTypes,
GetHeaderLabel = header => header.Label, GetHeaderLabel = header => header.Label,
DefaultHeader = SortTypes.FirstOrDefault(h => h.SortType == SortType.Ascending), DefaultHeaders = SortTypes.Where(h => h.SortType == SortType.Ascending).ToList(),
Items = GameProperties, Items = GameProperties,
GetItemLabel = item => item.Label, GetItemLabel = item => item.Label,
DefaultItem = GameProperties.FirstOrDefault(p => p.Label == "Titre") DefaultItems = GameProperties.Where(p => p.Label == "Titre").ToList()
}; };
} }

View File

@@ -24,6 +24,6 @@
<AdvancedGameFilter @bind-GameFilter=GameFilter Categories="Categories" /> <AdvancedGameFilter @bind-GameFilter=GameFilter Categories="Categories" />
</div> </div>
<Popup @ref=ManualAddPopup BackdropFilterClicked="HandleBackdropManualAddClicked"> <Popup @ref=ManualAddPopup BackdropFilterClicked="HandleBackdropManualAddClicked" Closable=false>
<GameCreationForm Categories="Categories" /> <GameCreationForm Categories="Categories" OnSubmit="HandleFetchCategories" />
</Popup> </Popup>

View File

@@ -17,7 +17,7 @@ public partial class GameBase ()
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
Categories = await GameGateway.FetchCategories(); await HandleFetchCategories();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
private void HandleAddClicked(AddType addType) private void HandleAddClicked(AddType addType)
@@ -37,4 +37,8 @@ public partial class GameBase ()
{ {
ManualAddPopup?.Close(); ManualAddPopup?.Close();
} }
private async Task HandleFetchCategories()
{
Categories = await GameGateway.FetchCategories();
}
} }

View File

@@ -8,6 +8,18 @@ namespace GameIdeas.BlazorApp.Pages.Games.Gateways;
public class GameGateway(IHttpClientService httpClientService) : IGameGateway public class GameGateway(IHttpClientService httpClientService) : IGameGateway
{ {
public async Task<int> CreateGame(GameDto game)
{
try
{
return await httpClientService.PostAsync<int>(Endpoints.Game.Create, game);
}
catch (Exception)
{
throw new GameCreationException(ResourcesKey.ErrorCreateGame);
}
}
public async Task<CategoriesDto> FetchCategories() public async Task<CategoriesDto> FetchCategories()
{ {
try try

View File

@@ -5,4 +5,5 @@ namespace GameIdeas.BlazorApp.Pages.Games.Gateways;
public interface IGameGateway public interface IGameGateway
{ {
Task<CategoriesDto> FetchCategories(); Task<CategoriesDto> FetchCategories();
Task<int> CreateGame(GameDto game);
} }

View File

@@ -23,8 +23,7 @@
</svg> </svg>
</div> </div>
<Select @ref="SelectListAdd" TItem="KeyValuePair<AddType, string>" THeader="object" <Select @ref="SelectListAdd" TItem="KeyValuePair<AddType, string>" THeader="object"
ValuesChanged=HandleAddTypeClicked Params=SelectParams ValuesChanged=HandleAddTypeClicked Params=SelectParams Theme="SelectTheme.Navigation">
Theme="SelectTheme.Navigation">
<div class="second-button button"> <div class="second-button button">
<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M1 3H23L12 22" /> <path d="M1 3H23L12 22" />

View File

@@ -28,6 +28,7 @@ public partial class GameHeader : ComponentBase
{ {
Items = AddTypes.ToList(), Items = AddTypes.ToList(),
GetItemLabel = item => item.Value, GetItemLabel = item => item.Value,
DefaultItems = []
}; };
base.OnInitialized(); base.OnInitialized();

View File

@@ -1,5 +1,9 @@
.backdrop-filter { .backdrop-filter {
position: fixed; position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
inset: 0; inset: 0;
z-index: var(--index-backdrop); z-index: var(--index-backdrop);
} }

View File

@@ -0,0 +1,3 @@
<div class="overlay"> <div class="loader"></div> </div>

View File

@@ -0,0 +1,31 @@
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--overlay);
}
.loader {
border: 8px solid var(--violet-selected);
border-top: 8px solid var(--violet);
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -2,17 +2,20 @@
@using GameIdeas.BlazorApp.Shared.Constants @using GameIdeas.BlazorApp.Shared.Constants
<CascadingValue Value="this"> <CascadingValue Value="this">
<div class="popup-wrapper" style="@GetDisplayStyle()"> @if (IsOpen)
<div class="popup-content"> {
@if (Closable) <div class="popup-wrapper">
{ <div class="popup-content">
<button @onclick="HandleBackdropFilterClicked">@Icons.Shared.Close</button> @if (Closable)
} {
@ChildContent <button @onclick="HandleBackdropFilterClicked">@Icons.Shared.Close</button>
}
@ChildContent
</div>
</div> </div>
</div> }
</CascadingValue> </CascadingValue>
<BackdropFilter @ref="BackdropFilter" OnClick="HandleBackdropFilterClicked" CloseOnClick="@Closable" <BackdropFilter @ref="BackdropFilter" OnClick="HandleBackdropFilterClicked" CloseOnClick="@Closable"
AllowBodyScroll="false" Color="BackdropFilterColor.Overlay" /> AllowBodyScroll="true" Color="BackdropFilterColor.Overlay" />

View File

@@ -5,11 +5,9 @@ namespace GameIdeas.BlazorApp.Shared.Components.Popup;
public partial class Popup public partial class Popup
{ {
[Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public bool IsDrawerOpen { get; set; }
[Parameter] public EventCallback BackdropFilterClicked { get; set; } [Parameter] public EventCallback BackdropFilterClicked { get; set; }
[Parameter] public bool Closable { get; set; } = true; [Parameter] public bool Closable { get; set; } = true;
public bool IsOpen { get; set; } public bool IsOpen { get; set; }
public EventHandler<bool>? StateChanged { get; set; }
private BackdropFilter.BackdropFilter? BackdropFilter; private BackdropFilter.BackdropFilter? BackdropFilter;
@@ -31,7 +29,6 @@ public partial class Popup
{ {
IsOpen = true; IsOpen = true;
await BackdropFilter?.Show()!; await BackdropFilter?.Show()!;
StateChanged?.Invoke(null, IsOpen);
StateHasChanged(); StateHasChanged();
} }
@@ -39,7 +36,6 @@ public partial class Popup
{ {
IsOpen = false; IsOpen = false;
await BackdropFilter?.Hide()!; await BackdropFilter?.Hide()!;
StateChanged?.Invoke(null, IsOpen);
StateHasChanged(); StateHasChanged();
} }
@@ -47,6 +43,4 @@ public partial class Popup
{ {
await BackdropFilterClicked.InvokeAsync(); await BackdropFilterClicked.InvokeAsync();
} }
private string GetDisplayStyle() => IsOpen ? "display: flex;" : "display: none;";
} }

View File

@@ -1,15 +1,13 @@
.popup-wrapper { .popup-wrapper {
display: none; display: flex;
justify-content: center; justify-self: anchor-center;
align-items: center; align-self: anchor-center;
position: fixed; position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: var(--index-popup); z-index: var(--index-popup);
} }
.popup-content { .popup-content {
overflow: hidden;
position: relative; position: relative;
background-color: var(--dropdown-content); background-color: var(--dropdown-content);
padding: 10px; padding: 10px;

View File

@@ -6,6 +6,8 @@
type="text" type="text"
class="search-field" class="search-field"
placeholder="@Placeholder" placeholder="@Placeholder"
disabled="@IsDisable"
style="@(IsDisable ? "pointer-events: none" : "")"
@bind=@Text @bind=@Text
@bind:event="oninput" @bind:event="oninput"
@bind:after=HandleTextChanged @bind:after=HandleTextChanged

View File

@@ -8,6 +8,7 @@ public partial class SearchInput
{ {
[Parameter] public string? Text { get; set; } [Parameter] public string? Text { get; set; }
[Parameter] public string? Placeholder { get; set; } [Parameter] public string? Placeholder { get; set; }
[Parameter] public bool IsDisable { get; set; }
[Parameter] public EventCallback<string> TextChanged { get; set; } [Parameter] public EventCallback<string> TextChanged { get; set; }
[Parameter] public EventCallback ClearClicked { get; set; } [Parameter] public EventCallback ClearClicked { get; set; }
[Parameter] public EventCallback SearchClicked { get; set; } [Parameter] public EventCallback SearchClicked { get; set; }
@@ -39,13 +40,17 @@ public partial class SearchInput
} }
private async Task HandleSearchClicked() private async Task HandleSearchClicked()
{ {
await TextChanged.InvokeAsync(Text); if (!IsDisable)
await SearchClicked.InvokeAsync(); {
await InputText.FocusAsync(); await SearchClicked.InvokeAsync();
}
} }
private async Task HandleFocusIn() private async Task HandleFocusIn()
{ {
await FocusIn.InvokeAsync(); if (!IsDisable)
{
await FocusIn.InvokeAsync();
}
} }
private MarkupString GetSearchIcon() private MarkupString GetSearchIcon()

View File

@@ -6,10 +6,10 @@
height: 20px; height: 20px;
align-items: center; align-items: center;
padding: 2px 6px; padding: 2px 6px;
cursor: pointer;
} }
.select-element:hover { .select-element:hover {
cursor: pointer;
background: var(--input-selected); background: var(--input-selected);
} }

View File

@@ -3,10 +3,11 @@
public class SelectParams<TItem, THeader> public class SelectParams<TItem, THeader>
{ {
public List<TItem> Items { get; set; } = []; public List<TItem> Items { get; set; } = [];
public TItem? DefaultItem { get; set; } public List<TItem> DefaultItems { get; set; } = [];
public Func<TItem, string> GetItemLabel { get; set; } = _ => string.Empty; public Func<TItem, string> GetItemLabel { get; set; } = _ => string.Empty;
public List<THeader> Headers { get; set; } = []; public List<THeader> Headers { get; set; } = [];
public THeader? DefaultHeader { get; set; } public List<THeader> DefaultHeaders { get; set; } = [];
public Func<THeader, string> GetHeaderLabel { get; set; } = _ => string.Empty; public Func<THeader, string> GetHeaderLabel { get; set; } = _ => string.Empty;
public Func<string, TItem>? AddItem { get; set; }
} }

View File

@@ -14,9 +14,20 @@
@if (IsContentOpen) @if (IsContentOpen)
{ {
<div class="content"> <div class="content">
@if (QuickAdd)
{
<div class="add-item">
<EditForm EditContext="QuickAddEditContext" OnSubmit="HandleSubmitAdd">
<input type="text" placeholder="@ResourcesKey.PlaceholderAdd" @bind=AddLabel>
</EditForm>
</div>
<span class="line"></span>
}
@if (Params.Headers != null) @if (Params.Headers != null)
{ {
@foreach (var header in Params.Headers) @foreach (var header in Params.Headers.Union(HeaderValues ?? []))
{ {
<SelectRow IsSelected=HeaderValues?.Contains(header) <SelectRow IsSelected=HeaderValues?.Contains(header)
Label="@Params.GetHeaderLabel(header)" Theme=Theme Label="@Params.GetHeaderLabel(header)" Theme=Theme
@@ -24,14 +35,14 @@
} }
} }
@if (Params.Headers?.Any() == true) @if (Params.Headers?.Count != 0)
{ {
<span class="line"></span> <span class="line"></span>
} }
@if (Params.Items != null) @if (Params.Items != null)
{ {
@foreach (var item in Params.Items) @foreach (var item in Params.Items.Union(Values ?? []))
{ {
<SelectRow IsSelected=Values?.Contains(item) <SelectRow IsSelected=Values?.Contains(item)
Label="@Params.GetItemLabel(item)" Theme=Theme Label="@Params.GetItemLabel(item)" Theme=Theme

View File

@@ -1,5 +1,9 @@
using GameIdeas.BlazorApp.Shared.Components.Select.Models; using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using GameIdeas.Resources;
using GameIdeas.Shared.Constants;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System.Text.RegularExpressions;
namespace GameIdeas.BlazorApp.Shared.Components.Select; namespace GameIdeas.BlazorApp.Shared.Components.Select;
@@ -14,37 +18,48 @@ public partial class Select<TItem, THeader>
[Parameter] public SelectTheme Theme { get; set; } [Parameter] public SelectTheme Theme { get; set; }
[Parameter] public SelectType Type { get; set; } = SelectType.Single; [Parameter] public SelectType Type { get; set; } = SelectType.Single;
[Parameter] public bool DisableClicked { get; set; } = false; [Parameter] public bool DisableClicked { get; set; } = false;
[Parameter] public bool QuickAdd { get; set; } = false;
private bool IsContentOpen = false; private bool IsContentOpen = false;
private string AddLabel = string.Empty;
private EditContext? QuickAddEditContext;
public void Close() => public void Close()
IsContentOpen = false;
public void Open() =>
IsContentOpen = true;
private void HandleButtonClicked()
{ {
if (!DisableClicked) IsContentOpen = false;
IsContentOpen = !IsContentOpen;
} }
private void HandleContentClosed() => public void Open()
IsContentOpen = false; {
IsContentOpen = true;
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
if (Params.DefaultItem != null) QuickAddEditContext = new EditContext(AddLabel);
if (Params.DefaultItems.Count != 0)
{ {
Values.Add(Params.DefaultItem); Values.AddRange(Params.DefaultItems);
} }
if (Params.DefaultHeader != null) if (Params.DefaultHeaders.Count != 0)
{ {
HeaderValues.Add(Params.DefaultHeader); HeaderValues.AddRange(Params.DefaultHeaders);
} }
} }
private void HandleButtonClicked()
{
if (!DisableClicked && IsContentOpen)
Close();
if (!DisableClicked && !IsContentOpen)
Open();
}
private void HandleContentClosed() => Close();
private async Task HandleValueClicked(TItem value) private async Task HandleValueClicked(TItem value)
{ {
if (Type != SelectType.Multiple || Values == null) if (Type != SelectType.Multiple || Values == null)
@@ -82,4 +97,16 @@ public partial class Select<TItem, THeader>
await HeaderValuesChanged.InvokeAsync(HeaderValues); await HeaderValuesChanged.InvokeAsync(HeaderValues);
} }
private async Task HandleSubmitAdd()
{
if (Params.AddItem != null)
{
Values ??= [];
Values.Add(Params.AddItem(AddLabel));
AddLabel = string.Empty;
await ValuesChanged.InvokeAsync(Values);
}
}
} }

View File

@@ -3,7 +3,8 @@
} }
.button { .button {
z-index: var(--index-component) z-index: var(--index-component);
cursor: pointer;
} }
.dropdown { .dropdown {
@@ -29,6 +30,20 @@
display: none; display: none;
} }
.add-item {
align-content: center;
height: 24px;
padding: 0 10px;
}
.add-item input {
width: 100%;
color: var(--white);
border: none;
outline: none;
background: transparent;
}
.dropdown::-webkit-scrollbar { .dropdown::-webkit-scrollbar {
width: 10px; width: 10px;
} }
@@ -72,4 +87,16 @@
border-bottom: 2px solid var(--input-selected); border-bottom: 2px solid var(--input-selected);
} }
/***** Sort Theme *****/
.creation .content {
border-radius: var(--small-radius);
box-sizing: border-box;
border: solid 1px var(--violet);
}
.creation .content .line {
display: block;
margin: 2px 6px;
border-bottom: 2px solid var(--input-selected);
}

View File

@@ -5,12 +5,12 @@
@typeparam TItem @typeparam TItem
<Select @ref=Select TItem="TItem" THeader="string" Theme="Theme" Type="SelectType.Multiple" DisableClicked=true <Select @ref=Select TItem="TItem" THeader="string" Theme="Theme" Type="SelectType.Multiple"
Params="SelectParams" Values=Values ValuesChanged="HandleValuesChanged"> Params="SelectParams" Values=Values ValuesChanged="HandleValuesChanged" QuickAdd=QuickAdd>
<div class="@SelectHelper.GetClassFromTheme(Theme)"> <div class="@SelectHelper.GetClassFromTheme(Theme)">
<SearchInput @ref=SearchInput Icon="SearchInputIcon.Dropdown" Placeholder="@Placeholder" <SearchInput @ref=SearchInput Icon="SearchInputIcon.Dropdown" Placeholder="@Placeholder"
TextChanged="HandleClearClicked" ClearClicked="HandleClearClicked" TextChanged="HandleClearClicked" ClearClicked="HandleClearClicked" IsDisable=QuickAdd
FocusIn="HandleFocusIn" SearchClicked="HandleFocusIn" /> FocusIn="HandleFocusIn" SearchClicked="HandleFocusIn" />
</div> </div>

View File

@@ -13,16 +13,20 @@ public partial class SelectSearch<TItem>
[Parameter] public List<TItem> Values { get; set; } = []; [Parameter] public List<TItem> Values { get; set; } = [];
[Parameter] public EventCallback<List<TItem>> ValuesChanged { get; set; } [Parameter] public EventCallback<List<TItem>> ValuesChanged { get; set; }
[Parameter] public string Placeholder { get; set; } = string.Empty; [Parameter] public string Placeholder { get; set; } = string.Empty;
[Parameter] public bool QuickAdd { get; set; } = false;
[Parameter] public Func<string, TItem>? AddItem { get; set; }
private SelectParams<TItem, string> SelectParams = new(); private SelectParams<TItem, string> SelectParams = new();
private SearchInput? SearchInput; private SearchInput? SearchInput;
private Select<TItem, string>? Select; private Select<TItem, string>? Select;
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
SelectParams = new() SelectParams = new()
{ {
Items = Items, Items = Items,
GetItemLabel = GetLabel GetItemLabel = GetLabel,
AddItem = AddItem
}; };
base.OnParametersSet(); base.OnParametersSet();
@@ -31,12 +35,13 @@ public partial class SelectSearch<TItem>
{ {
Values = values.ToList(); Values = values.ToList();
SearchInput?.SetText(string.Join(", ", Values.Select(GetLabel))); SearchInput?.SetText(string.Join(", ", Values.Select(GetLabel)));
await ValuesChanged.InvokeAsync(values.ToList()); await ValuesChanged.InvokeAsync(Values.ToList());
} }
private void HandleClearClicked() private async Task HandleClearClicked()
{ {
Values = []; Values = [];
await ValuesChanged.InvokeAsync(Values.ToList());
} }
private void HandleFocusIn() private void HandleFocusIn()

View File

@@ -1,7 +1,7 @@
.container { .container {
position: relative; position: relative;
width: 100%; width: 100%;
z-index: var(--index-component) z-index: 0
} }
input[type="range"] { input[type="range"] {

View File

@@ -1,7 +1,7 @@
.container { .container {
position: relative; position: relative;
width: 100%; width: 100%;
z-index: var(--index-component) z-index: 0
} }
input[type="range"] { input[type="range"] {

View File

@@ -4,7 +4,7 @@ public static class Endpoints
{ {
public static class Game public static class Game
{ {
public static readonly string Create = "api/Game/Create";
} }
public static class Category public static class Category

View File

@@ -0,0 +1,3 @@
namespace GameIdeas.BlazorApp.Shared.Exceptions;
public class GameCreationException(string message) : Exception(message);

View File

@@ -27,6 +27,7 @@
--index-backdrop: 700; --index-backdrop: 700;
--index-dropdown: 900; --index-dropdown: 900;
--index-popup: 1000; --index-popup: 1000;
--index-overlay: 1100;
} }
html { html {
@@ -41,10 +42,6 @@ html, body, #app {
height: 100%; height: 100%;
} }
.valid.modified:not([type=checkbox]) {
border: 2px solid var(--green);
}
.invalid { .invalid {
border: 2px solid var(--red) !important; border: 2px solid var(--red) !important;
} }
@@ -65,7 +62,7 @@ html, body, #app {
padding: 0.6rem 1.25rem 0.7rem 1.25rem; padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed; position: fixed;
width: 100%; width: 100%;
z-index: 1000; z-index: 10000;
} }
#blazor-error-ui .dismiss { #blazor-error-ui .dismiss {

View File

@@ -34,6 +34,10 @@ public class Translations (TranslationService translationService)
public string ErrorWhenFetchingData => translationService.Translate(nameof(ErrorWhenFetchingData)); public string ErrorWhenFetchingData => translationService.Translate(nameof(ErrorWhenFetchingData));
public string RequestFailedStatusFormat => translationService.Translate(nameof(RequestFailedStatusFormat)); public string RequestFailedStatusFormat => translationService.Translate(nameof(RequestFailedStatusFormat));
public string ErrorFetchCategories => translationService.Translate(nameof(ErrorFetchCategories)); public string ErrorFetchCategories => translationService.Translate(nameof(ErrorFetchCategories));
public string PlaceholderAdd => translationService.Translate(nameof(PlaceholderAdd));
public string ErrorCreateGame => translationService.Translate(nameof(ErrorCreateGame));
public string InvalidTitle => translationService.Translate(nameof(InvalidTitle));
public string InvalidInterest => translationService.Translate(nameof(InvalidInterest));
} }
public static class ResourcesKey public static class ResourcesKey
@@ -76,4 +80,8 @@ public static class ResourcesKey
public static string ErrorWhenFetchingData => _instance?.ErrorWhenFetchingData ?? throw new InvalidOperationException("ResourcesKey.ErrorWhenFetchingData is not initialized."); public static string ErrorWhenFetchingData => _instance?.ErrorWhenFetchingData ?? throw new InvalidOperationException("ResourcesKey.ErrorWhenFetchingData is not initialized.");
public static string RequestFailedStatusFormat => _instance?.RequestFailedStatusFormat ?? throw new InvalidOperationException("ResourcesKey.RequestFailedStatusFormat is not initialized."); public static string RequestFailedStatusFormat => _instance?.RequestFailedStatusFormat ?? throw new InvalidOperationException("ResourcesKey.RequestFailedStatusFormat is not initialized.");
public static string ErrorFetchCategories => _instance?.ErrorFetchCategories ?? throw new InvalidOperationException("ResourcesKey.ErrorFetchCategories is not initialized."); public static string ErrorFetchCategories => _instance?.ErrorFetchCategories ?? throw new InvalidOperationException("ResourcesKey.ErrorFetchCategories is not initialized.");
public static string PlaceholderAdd => _instance?.PlaceholderAdd ?? throw new InvalidOperationException("ResourcesKey.PlaceholderAdd is not initialized.");
public static string ErrorCreateGame => _instance?.ErrorCreateGame ?? throw new InvalidOperationException("ResourcesKey.ErrorCreateGame is not initialized.");
public static string InvalidTitle => _instance?.InvalidTitle ?? throw new InvalidOperationException("ResourcesKey.InvalidTitle is not initialized.");
public static string InvalidInterest => _instance?.InvalidInterest ?? throw new InvalidOperationException("ResourcesKey.InvalidInterest is not initialized.");
} }

View File

@@ -2,6 +2,6 @@
public class GlobalConstants public class GlobalConstants
{ {
public const string EnterKeyCode = "Enter";
public const string PadEnterKeyCode = "NumpadEnter";
} }

View File

@@ -40,11 +40,12 @@ public class GameController(IGameService gameService, ILoggerFactory loggerFacto
} }
[HttpPost("Create")] [HttpPost("Create")]
public async Task<ActionResult<GameDto>> CreateGame([FromBody] GameDto game) public async Task<ActionResult<int>> CreateGame([FromBody] GameDto game)
{ {
try try
{ {
return Created("/Create", await gameService.CreateGame(game)); var gameResult = await gameService.CreateGame(game);
return Created("/Create", gameResult.Id);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -54,11 +55,12 @@ public class GameController(IGameService gameService, ILoggerFactory loggerFacto
} }
[HttpPut("Update")] [HttpPut("Update")]
public async Task<ActionResult<GameDto>> UpdateGame([FromBody] GameDto game) public async Task<ActionResult<int>> UpdateGame([FromBody] GameDto game)
{ {
try try
{ {
return Created($"/Update", await gameService.UpdateGame(game)); var gameResult = await gameService.UpdateGame(game);
return Created($"/Update", gameResult.Id);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -29,5 +29,9 @@
"ErrorWhenDeletingData": "Erreur lors de la requête DELETE", "ErrorWhenDeletingData": "Erreur lors de la requête DELETE",
"ErrorWhenFetchingData": "Erreur lors de la requête GET", "ErrorWhenFetchingData": "Erreur lors de la requête GET",
"RequestFailedStatusFormat": "Erreur lors de la réponse, code {0}", "RequestFailedStatusFormat": "Erreur lors de la réponse, code {0}",
"ErrorFetchCategories": "Erreur lors de la récupération des catégories" "ErrorFetchCategories": "Erreur lors de la récupération des catégories",
"PlaceholderAdd": "Ajouter un nouveau",
"ErrorCreateGame": "Erreur lors de la Création d'un jeu",
"InvalidTitle": "Le titre est incorrect",
"InvalidInterest": "L'interêt est incorrect'"
} }