diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Helpers/GameHelper.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Helpers/GameHelper.cs index 27df03e..9778a14 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Helpers/GameHelper.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Helpers/GameHelper.cs @@ -20,6 +20,20 @@ public static class GameHelper game.CreationDate = DateTime.Now; } + public static void UpdateTrackingDto(GameDetailDto game, AuthenticationState authState) + { + if (authState == null) + { + throw new ArgumentNullException(nameof(authState), "Authentication state missing"); + } + + var userId = authState.User.FindFirstValue(ClaimTypes.Sid) + ?? throw new ArgumentNullException(nameof(authState), "user state missing"); + + game.ModificationUserId = userId; + game.ModificationDate = DateTime.Now; + } + public static string GetInterestColor(int interest, int maxInterest) { int firstTier = (int)Math.Floor(0.33 * maxInterest); diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor index 9682b3f..052f459 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor @@ -1,14 +1,22 @@ @page "/Detail/{GameId:int}" @using GameIdeas.BlazorApp.Helpers +@using GameIdeas.BlazorApp.Pages.Games.Components +@using GameIdeas.BlazorApp.Shared.Components +@using GameIdeas.BlazorApp.Shared.Components.ButtonAdd @using GameIdeas.BlazorApp.Shared.Components.Header @using GameIdeas.BlazorApp.Shared.Components.Interest +@using GameIdeas.BlazorApp.Shared.Components.Popup @using GameIdeas.BlazorApp.Shared.Components.ReadMore @using GameIdeas.BlazorApp.Shared.Constants @using GameIdeas.Shared.Constants + +@inherits GameBaseComponent @layout MainLayout - +
+ +
@@ -97,4 +105,8 @@
-
\ No newline at end of file + + + + + \ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor.cs index 035976c..9e28609 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor.cs @@ -1,19 +1,45 @@ -using GameIdeas.BlazorApp.Pages.Games.Gateways; +using GameIdeas.BlazorApp.Shared.Components; +using GameIdeas.BlazorApp.Shared.Exceptions; +using GameIdeas.Resources; using GameIdeas.Shared.Dto; using Microsoft.AspNetCore.Components; +using System.Linq.Expressions; namespace GameIdeas.BlazorApp.Pages.Detail; -public partial class GameDetail +public partial class GameDetail : GameBaseComponent { - [Inject] private IGameGateway GameGateway { get; set; } = default!; + [Inject] private NavigationManager NavigationManager { get; set; } = default!; [Parameter] public int GameId { get; set; } private GameDetailDto Game = new(); protected override async Task OnInitializedAsync() { - Game = await GameGateway.GetGameById(GameId); + await FetchGameDetail(); await base.OnInitializedAsync(); } + + private void HandleSubmitNewGame() + { + NavigationManager.NavigateTo("/"); + } + + private async Task FetchGameDetail() + { + try + { + IsLoading = true; + + Game = await GameGateway.GetGameById(GameId); + } + catch (Exception) + { + throw new FetchGameDetailException(ResourcesKey.ErrorFetchDetail); + } + finally + { + IsLoading = false; + } + } } \ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor.css b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor.css index 430cb0f..af35504 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor.css +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Detail/GameDetail.razor.css @@ -90,7 +90,13 @@ background: var(--input-selected); } -@media screen and (min-width: 700px) and (max-width: 1000px) { +.button { + display: flex; + width: 100%; + justify-content: end; +} + +@media screen and (max-width: 1000px) { .section { padding: 20px; } diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/DetailOptions.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/DetailOptions.cs new file mode 100644 index 0000000..4913684 --- /dev/null +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/DetailOptions.cs @@ -0,0 +1,8 @@ +namespace GameIdeas.BlazorApp.Pages.Games.Components; + +public enum DetailOptions +{ + Detail, + Edit, + Delete +} diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameBase.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameBase.cs index 14e4339..938d9f3 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameBase.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameBase.cs @@ -1,4 +1,7 @@ -using GameIdeas.Shared.Dto; +using GameIdeas.BlazorApp.Shared.Components.Select; +using GameIdeas.BlazorApp.Shared.Components.Select.Models; +using GameIdeas.Resources; +using GameIdeas.Shared.Dto; using Microsoft.AspNetCore.Components; namespace GameIdeas.BlazorApp.Pages.Games.Components; @@ -6,10 +9,51 @@ namespace GameIdeas.BlazorApp.Pages.Games.Components; public class GameBase : ComponentBase { [Parameter] public GameDto GameDto { get; set; } = new(); + [Parameter] public EventCallback OnDelete { get; set; } = new(); + [Parameter] public EventCallback OnEdit { get; set; } = new(); [Inject] public NavigationManager NavigationManager { get; set; } = default!; - protected void HandleDetailClicked() + protected SelectParams SelectParams = default!; + protected Select? SelectOption; + + protected override void OnInitialized() { - NavigationManager.NavigateTo($"/Games/Detail/{GameDto.Id}"); + SelectParams = new() + { + Items = [DetailOptions.Detail, DetailOptions.Edit, DetailOptions.Delete], + GetItemLabel = GetDetailOptionsLabel + }; + } + + protected async Task HandlerSelectValuesChanged(IEnumerable detailOptions) + { + var option = detailOptions.First(); + switch (option) + { + case DetailOptions.Detail: + NavigationManager.NavigateTo($"/Detail/{GameDto.Id}"); + break; + case DetailOptions.Edit: + await OnEdit.InvokeAsync(GameDto); + break; + case DetailOptions.Delete: + await OnDelete.InvokeAsync(GameDto); + break; + default: + break; + } + + SelectOption?.Close(); + } + + private string GetDetailOptionsLabel(DetailOptions options) + { + return options switch + { + DetailOptions.Detail => ResourcesKey.Detail, + DetailOptions.Edit => ResourcesKey.Edit, + DetailOptions.Delete => ResourcesKey.Delete, + _ => ResourcesKey.Unknown + }; } } diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor index c9e1028..419c02a 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor @@ -11,7 +11,7 @@
@ResourcesKey.Title :
- +
@ResourcesKey.ReleaseDate :
@@ -24,13 +24,13 @@
@ResourcesKey.Developer :
@ResourcesKey.Publisher :
@@ -44,21 +44,21 @@
@ResourcesKey.Properties :
+ Items="Categories?.Properties" @bind-Values=GameDto.Properties + AddItem="@(str => new PropertyDto() { Label = str })" />
@ResourcesKey.Tags :
+ Items="Categories?.Tags" @bind-Values=GameDto.Tags + AddItem="@(str => new TagDto() { Label = str })" />
@ResourcesKey.Platforms :
+ Items="Categories?.Platforms" @bind-Values=GameDto.Platforms + AddItem="@(str => new PlatformDto() { Label = str })" />
@foreach (var platform in GameDto.Platforms ?? []) diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.cs index e7d107f..50cb2dc 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.cs @@ -3,6 +3,8 @@ using GameIdeas.BlazorApp.Pages.Games.Gateways; using GameIdeas.BlazorApp.Shared.Components.Popup; using GameIdeas.BlazorApp.Shared.Components.Select.Models; using GameIdeas.BlazorApp.Shared.Components.Slider; +using GameIdeas.BlazorApp.Shared.Exceptions; +using GameIdeas.Resources; using GameIdeas.Shared.Dto; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; @@ -19,22 +21,27 @@ public partial class GameCreationForm [CascadingParameter] private Popup? Popup { get; set; } [Parameter] public CategoriesDto? Categories { get; set; } [Parameter] public EventCallback OnSubmit { get; set; } - - private readonly GameDetailDto GameDto = new(); + [Parameter] public EventCallback OnRender { get; set; } + + private GameDetailDto GameDto = new(); private EditContext? EditContext; private readonly SelectTheme Theme = SelectTheme.Creation; private readonly SliderParams SliderParams = new() { Gap = 1, Min = 1, Max = 5 }; private bool IsLoading = false; - protected override async Task OnInitializedAsync() + protected override void OnInitialized() { EditContext = new(GameDto); - await base.OnInitializedAsync(); + base.OnInitialized(); } protected override async Task OnAfterRenderAsync(bool firstRender) { await Js.InvokeVoidAsync("resizeGameForm"); + if (firstRender) + { + await OnRender.InvokeAsync(); + } } private void HandleOnCancel() @@ -53,15 +60,22 @@ public partial class GameCreationForm { IsLoading = true; + int gameId; var authState = await AuthenticationState.GetAuthenticationStateAsync(); GameHelper.WriteTrackingDto(GameDto, authState); - var gameId = await GameGateway.CreateGame(GameDto); - - if (gameId != 0) + if (GameDto.Id != null) { - Popup?.Close(); - await OnSubmit.InvokeAsync(); + gameId = await GameGateway.UpdateGame(GameDto); + } + else + { + gameId = await GameGateway.CreateGame(GameDto); + } + + if (gameId == 0) + { + throw new GameCreationException(ResourcesKey.ErrorCreateGame); } } catch (Exception) @@ -73,13 +87,40 @@ public partial class GameCreationForm IsLoading = false; StateHasChanged(); } + + Popup?.Close(); + + await OnSubmit.InvokeAsync(); } + private void HandlePublisherChanged(List pubs) { GameDto.Publisher = pubs.FirstOrDefault(); } + private void HandleDeveloperChanged(List devs) { GameDto.Developer = devs.FirstOrDefault(); } + + public async Task SetGameToUpdateAsync(int gameId) + { + try + { + IsLoading = true; + + GameDto = await GameGateway.GetGameById(gameId); + } + catch (Exception) + { + throw new FetchGameDetailException(ResourcesKey.ErrorFetchDetail); + } + finally + { + IsLoading = false; + } + + EditContext = new(GameDto); + StateHasChanged(); + } } \ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.css b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.css index b512d37..4d3a824 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.css +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.css @@ -118,3 +118,20 @@ .buttons button:hover { background: var(--violet-selected); } + +@media screen and (max-width: 400px) { + .input-game { + grid-template-columns: auto 1fr; + } + + #label-description { + width: auto !important; + } +} + +@media screen and (max-width: 700px) { + .game-form { + flex-direction: column; + gap: 8px; + } +} \ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameRow.razor b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameRow.razor index 5ecb8ac..059bc90 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameRow.razor +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameRow.razor @@ -1,5 +1,7 @@ @using GameIdeas.BlazorApp.Helpers @using GameIdeas.BlazorApp.Shared.Components.Interest +@using GameIdeas.BlazorApp.Shared.Components.Select +@using GameIdeas.BlazorApp.Shared.Components.Select.Models @using GameIdeas.BlazorApp.Shared.Constants @inherits GameBase @@ -33,5 +35,8 @@ - +
\ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameRow.razor.css b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameRow.razor.css index 6d2fab0..fd6ce91 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameRow.razor.css +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameRow.razor.css @@ -8,7 +8,6 @@ box-shadow: var(--drop-shadow); border-radius: var(--big-radius); align-items: center; - overflow: hidden; } .row > * { @@ -69,17 +68,23 @@ text-decoration: underline; } -.detail { - transform: scale(0.6, 1) rotate(-90deg); - background: none; - border: none; - outline: none; - cursor: pointer; +::deep .button { + width: fit-content; + transform: rotate(-90deg); + transition: transform 0.2s ease-in-out; + justify-self: center; } -::deep .detail svg { - fill: var(--white); -} + ::deep .button svg { + fill: var(--white); + height: 20px; + width: 20px; + transform: scale(1, 0.6); + } + + ::deep .button:hover, ::deep .button.selected { + transform: translate(-4px, 2px); + } @media screen and (max-width: 700px) { .release-date, .tags, .storage { diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Games.razor b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Games.razor index 995bafd..73b7dc7 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Games.razor +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Games.razor @@ -5,8 +5,9 @@ @using GameIdeas.BlazorApp.Shared.Components.ButtonAdd @using GameIdeas.BlazorApp.Shared.Components.Header @using GameIdeas.BlazorApp.Shared.Components.Popup +@using GameIdeas.BlazorApp.Shared.Components.Popup.Components @using GameIdeas.Resources - +@inherits GameBaseComponent @layout MainLayout @ResourcesKey.GamesIdeas @@ -24,7 +25,7 @@ { @foreach (var game in GamesDto) { - + } } else @@ -41,5 +42,9 @@ - + + + + + \ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Games.razor.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Games.razor.cs index 3a334de..dc72481 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Games.razor.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Games.razor.cs @@ -1,24 +1,23 @@ +using GameIdeas.BlazorApp.Pages.Games.Components; using GameIdeas.BlazorApp.Pages.Games.Filter; -using GameIdeas.BlazorApp.Pages.Games.Gateways; +using GameIdeas.BlazorApp.Shared.Components; using GameIdeas.BlazorApp.Shared.Components.Popup; using GameIdeas.BlazorApp.Shared.Models; using GameIdeas.Shared.Dto; using GameIdeas.Shared.Enum; -using Microsoft.AspNetCore.Components; namespace GameIdeas.BlazorApp.Pages.Games; -public partial class Games +public partial class Games : GameBaseComponent { - [Inject] private IGameGateway GameGateway { get; set; } = default!; - private DisplayType DisplayType = DisplayType.List; private GameFilterParams GameFilter = new(); - private Popup? ManualAddPopup; - private bool IsLoading = false; - private CategoriesDto? Categories; private IEnumerable GamesDto = []; private int CurrentPage; + private Popup? DeletePopup; + private GameDto? GameToDelete; + private int? GameIdToUpdate; + private GameCreationForm? CreationForm; protected override async Task OnInitializedAsync() { @@ -33,32 +32,12 @@ public partial class Games await base.OnInitializedAsync(); } - private void HandleAddClicked(AddType addType) - { - switch (addType) - { - case AddType.Manual: - ManualAddPopup?.Open(); - break; - case AddType.Auto: - break; - default: - break; - } - } - private void HandleBackdropManualAddClicked() - { - ManualAddPopup?.Close(); - } - private async Task HandleFetchDatas(bool loadCategories = true, bool displayLoader = true) + private async Task HandleFetchDatas(bool displayLoader = true) { try { IsLoading = displayLoader; - if (loadCategories) - Categories = await GameGateway.FetchCategories(); - GamesDto = await GameGateway.FetchGames(GameFilter, CurrentPage); } catch (Exception) @@ -73,6 +52,68 @@ public partial class Games private async Task HandleFilterChanged(GameFilterParams args) { GameFilter = args; - await HandleFetchDatas(loadCategories: false, displayLoader: false); + await HandleFetchDatas(false); + } + + private void HandleDeleteGame(GameDto args) + { + DeletePopup?.Open(); + GameToDelete = args; + } + + private void HandleCancelPopupClicked() + { + DeletePopup?.Close(); + GameToDelete = null; + } + + private void HandleEditGame(GameDto args) + { + if (args.Id == null) + { + return; + } + + GameIdToUpdate = args.Id; + + ManualAddPopup?.Open(); + } + + private async Task HandleRemoveGame() + { + DeletePopup?.Close(); + + if (GameToDelete?.Id == null) + { + return; + } + + try + { + IsLoading = true; + + await GameGateway.DeleteGame(GameToDelete?.Id ?? 0); + await HandleFetchDatas(false); + } + catch (Exception) + { + throw; + } + + GameToDelete = null; + } + private async Task HandleRenderCreationForm() + { + if (GameIdToUpdate != null && CreationForm != null) + { + await CreationForm.SetGameToUpdateAsync(GameIdToUpdate ?? 0); + } + + GameIdToUpdate = null; + } + private async Task HandleOnSubmitGame() + { + await HandleFetchDatas(false); + await HandleFetchCategories(); } } \ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Gateways/GameGateway.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Gateways/GameGateway.cs index 12f57d2..0affe3f 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Gateways/GameGateway.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Gateways/GameGateway.cs @@ -76,4 +76,28 @@ public class GameGateway(IHttpClientService httpClientService) : IGameGateway throw new CategoryNotFoundException(ResourcesKey.ErrorFetchGames); } } + + public async Task DeleteGame(int gameIdToDelete) + { + try + { + return await httpClientService.DeleteAsync(Endpoints.Game.Delete(gameIdToDelete)); + } + catch (Exception) + { + throw new GameDeletionException(ResourcesKey.ErrorDeleteGame); + } + } + + public async Task UpdateGame(GameDetailDto gameDto) + { + try + { + return await httpClientService.PutAsync(Endpoints.Game.Update, gameDto); + } + catch (Exception) + { + throw new GameUpdateException(ResourcesKey.ErrorUpdateGame); + } + } } diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Gateways/IGameGateway.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Gateways/IGameGateway.cs index 9bc4fc4..ac8cb99 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Gateways/IGameGateway.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Gateways/IGameGateway.cs @@ -9,4 +9,6 @@ public interface IGameGateway Task CreateGame(GameDetailDto game); Task> FetchGames(GameFilterParams filter, int currentPage); Task GetGameById(int gameId); + Task DeleteGame(int gameIdToDelete); + Task UpdateGame(GameDetailDto gameDto); } diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Users/Components/UserRow.razor.css b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Users/Components/UserRow.razor.css index d82d6f8..a3a4251 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Users/Components/UserRow.razor.css +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Users/Components/UserRow.razor.css @@ -89,3 +89,16 @@ .submit ::deep svg { fill: var(--green); } + +@media screen and (max-width: 700px) { + .row { + height: 104px; + display: grid; + grid-template-rows: 48px 48px; + grid-template-columns: 48px 1fr 100px; + } + + .role { + grid-column: 2; + } +} diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/GameBaseComponent.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/GameBaseComponent.cs new file mode 100644 index 0000000..54fee3a --- /dev/null +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/GameBaseComponent.cs @@ -0,0 +1,58 @@ +using GameIdeas.BlazorApp.Pages.Games.Gateways; +using GameIdeas.BlazorApp.Shared.Models; +using GameIdeas.Shared.Dto; +using Microsoft.AspNetCore.Components; + +namespace GameIdeas.BlazorApp.Shared.Components; + +public class GameBaseComponent : ComponentBase +{ + [Inject] protected IGameGateway GameGateway { get; set; } = default!; + + protected Popup.Popup? ManualAddPopup; + protected CategoriesDto? Categories; + protected bool IsLoading = false; + + protected override async Task OnInitializedAsync() + { + await HandleFetchCategories(); + await base.OnInitializedAsync(); + } + + protected async Task HandleFetchCategories() + { + try + { + IsLoading = true; + + Categories = await GameGateway.FetchCategories(); + } + catch (Exception) + { + throw; + } + finally + { + IsLoading = false; + } + } + + protected void HandleAddClicked(AddType addType) + { + switch (addType) + { + case AddType.Manual: + ManualAddPopup?.Open(); + break; + case AddType.Auto: + break; + default: + break; + } + } + + protected void HandleBackdropManualAddClicked() + { + ManualAddPopup?.Close(); + } +} diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Components/SelectRow.razor.css b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Components/SelectRow.razor.css index 5651f0a..50f47b0 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Components/SelectRow.razor.css +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Components/SelectRow.razor.css @@ -51,4 +51,21 @@ .single .selected { display: none; +} + +/***** Navigation Theme *****/ +.row-option { + padding: 4px 8px; +} + + .row-option:hover { + background: var(--violet-selected); + } + + .row-option .selected { + display: none; + } + +.row-option:last-child { + color: var(--red); } \ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Helpers/SelectHelper.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Helpers/SelectHelper.cs index a327106..e184354 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Helpers/SelectHelper.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Helpers/SelectHelper.cs @@ -14,6 +14,7 @@ public static class SelectHelper SelectTheme.AdvancedFilter => "advanced-filter", SelectTheme.Creation => "creation", SelectTheme.Single => "single", + SelectTheme.RowOption => "row-option", _ => string.Empty }; } diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Models/SelectTheme.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Models/SelectTheme.cs index c5af5a2..fb09cf1 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Models/SelectTheme.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Models/SelectTheme.cs @@ -7,5 +7,6 @@ public enum SelectTheme Filter, AdvancedFilter, Creation, - Single + Single, + RowOption } diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor index 04bf62a..1ab6f65 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor @@ -6,7 +6,7 @@ @typeparam THeader
-
+
@ChildContent
@@ -27,9 +27,9 @@ @if (Params.Headers != null) { - @foreach (var header in Params.Headers.Union(HeaderValues ?? [])) + @foreach (var header in (HeaderValues ?? []).UnionBy(Params.Headers, Params.GetHeaderLabel)) { - } @@ -42,9 +42,9 @@ @if (Params.Items != null) { - @foreach (var item in Params.Items.Union(Values ?? [])) + @foreach (var item in (Values ?? []).UnionBy(Params.Items, Params.GetItemLabel)) { - } diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor.cs index 9f2680c..faef52e 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor.cs @@ -89,8 +89,13 @@ public partial class Select if (Params.AddItem != null) { Values ??= []; - Values.Add(Params.AddItem(AddLabel)); + if (Type != SelectType.Multiple) + { + Values = []; + } + + Values.Add(Params.AddItem(AddLabel)); AddLabel = string.Empty; await ValuesChanged.InvokeAsync(Values); diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor.css b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor.css index eec629c..4faecde 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor.css +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/Select/Select.razor.css @@ -104,3 +104,13 @@ .single { border: none; } + +/***** Row Option Theme *****/ +.dropdown.row-option { + width: auto; + right: 10px; +} + +.row-option .content { + background: var(--violet); +} diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/SelectSearch/SelectSearch.razor.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/SelectSearch/SelectSearch.razor.cs index 65d6e57..466d708 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/SelectSearch/SelectSearch.razor.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Components/SelectSearch/SelectSearch.razor.cs @@ -32,6 +32,15 @@ public partial class SelectSearch base.OnParametersSet(); } + + protected override void OnAfterRender(bool firstRender) + { + if (Values != null) + { + SearchInput?.SetText(string.Join(", ", Values.Select(GetLabel))); + } + } + private async Task HandleValuesChanged(IEnumerable values) { Values = [.. values]; diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Constants/Endpoints.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Constants/Endpoints.cs index c16d19b..1447fb6 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Constants/Endpoints.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Constants/Endpoints.cs @@ -10,6 +10,8 @@ public static class Endpoints public const string Create = "api/Game/Create"; public static string Fetch(GameFilterDto filter) => $"api/Game?{UrlHelper.BuildUrlParams(filter)}"; public static string FetchById(int gameId) => $"api/Game/{gameId}"; + public static string Delete(int gameId) => $"api/Game/Delete/{gameId}"; + public const string Update = "api/Game/Update"; } public static class Category diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Exceptions/FetchGameDetailException.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Exceptions/FetchGameDetailException.cs new file mode 100644 index 0000000..94e49d5 --- /dev/null +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Exceptions/FetchGameDetailException.cs @@ -0,0 +1,3 @@ +namespace GameIdeas.BlazorApp.Shared.Exceptions; + +public class FetchGameDetailException(string message) : Exception(message); \ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Exceptions/GameDeletionException.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Exceptions/GameDeletionException.cs new file mode 100644 index 0000000..50f5175 --- /dev/null +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Exceptions/GameDeletionException.cs @@ -0,0 +1,3 @@ +namespace GameIdeas.BlazorApp.Shared.Exceptions; + +public class GameDeletionException(string message) : Exception(message); diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Exceptions/GameUpdateException.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Exceptions/GameUpdateException.cs new file mode 100644 index 0000000..628a3bb --- /dev/null +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Shared/Exceptions/GameUpdateException.cs @@ -0,0 +1,3 @@ +namespace GameIdeas.BlazorApp.Shared.Exceptions; + +public class GameUpdateException(string message) : Exception(message); diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/wwwroot/css/app.css b/src/GameIdeas/Client/GameIdeas.BlazorApp/wwwroot/css/app.css index c443f39..e3bf462 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/wwwroot/css/app.css +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/wwwroot/css/app.css @@ -147,9 +147,10 @@ code { .body-lg { font-weight: 400; font-size: 14px; + display: block; } -.header-1, .header-2, span, a { +.header-1, .header-2 { display: block; color: var(--white); margin: 0; diff --git a/src/GameIdeas/GameIdeas.Resources/CreateStaticResourceKey.cs b/src/GameIdeas/GameIdeas.Resources/CreateStaticResourceKey.cs index d40c808..59d4532 100644 --- a/src/GameIdeas/GameIdeas.Resources/CreateStaticResourceKey.cs +++ b/src/GameIdeas/GameIdeas.Resources/CreateStaticResourceKey.cs @@ -1,7 +1,7 @@ namespace GameIdeas.Resources; -public class Translations(TranslationService translationService) +public class Translations (TranslationService translationService) { public string GamesIdeas => translationService.Translate(nameof(GamesIdeas)); public string ManualAdd => translationService.Translate(nameof(ManualAdd)); @@ -39,10 +39,13 @@ public class Translations(TranslationService translationService) public string ErrorFetchCategories => translationService.Translate(nameof(ErrorFetchCategories)); public string PlaceholderAdd => translationService.Translate(nameof(PlaceholderAdd)); public string ErrorCreateGame => translationService.Translate(nameof(ErrorCreateGame)); + public string ErrorDeleteGame => translationService.Translate(nameof(ErrorDeleteGame)); + public string ErrorUpdateGame => translationService.Translate(nameof(ErrorUpdateGame)); public string InvalidTitle => translationService.Translate(nameof(InvalidTitle)); public string InvalidInterest => translationService.Translate(nameof(InvalidInterest)); public string Unknown => translationService.Translate(nameof(Unknown)); public string ErrorFetchGames => translationService.Translate(nameof(ErrorFetchGames)); + public string ErrorFetchDetail => translationService.Translate(nameof(ErrorFetchDetail)); public string Ascending => translationService.Translate(nameof(Ascending)); public string Descending => translationService.Translate(nameof(Descending)); public string ErrorStorageSpaceLabel => translationService.Translate(nameof(ErrorStorageSpaceLabel)); @@ -68,6 +71,9 @@ public class Translations(TranslationService translationService) public string About => translationService.Translate(nameof(About)); public string ReadMore => translationService.Translate(nameof(ReadMore)); public string ReadLess => translationService.Translate(nameof(ReadLess)); + public string Detail => translationService.Translate(nameof(Detail)); + public string Edit => translationService.Translate(nameof(Edit)); + public string Delete => translationService.Translate(nameof(Delete)); } public static class ResourcesKey @@ -115,10 +121,13 @@ public static class ResourcesKey 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 ErrorDeleteGame => _instance?.ErrorDeleteGame ?? throw new InvalidOperationException("ResourcesKey.ErrorDeleteGame is not initialized."); + public static string ErrorUpdateGame => _instance?.ErrorUpdateGame ?? throw new InvalidOperationException("ResourcesKey.ErrorUpdateGame 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."); public static string Unknown => _instance?.Unknown ?? throw new InvalidOperationException("ResourcesKey.Unknown is not initialized."); public static string ErrorFetchGames => _instance?.ErrorFetchGames ?? throw new InvalidOperationException("ResourcesKey.ErrorFetchGames is not initialized."); + public static string ErrorFetchDetail => _instance?.ErrorFetchDetail ?? throw new InvalidOperationException("ResourcesKey.ErrorFetchDetail is not initialized."); public static string Ascending => _instance?.Ascending ?? throw new InvalidOperationException("ResourcesKey.Ascending is not initialized."); public static string Descending => _instance?.Descending ?? throw new InvalidOperationException("ResourcesKey.Descending is not initialized."); public static string ErrorStorageSpaceLabel => _instance?.ErrorStorageSpaceLabel ?? throw new InvalidOperationException("ResourcesKey.ErrorStorageSpaceLabel is not initialized."); @@ -144,4 +153,7 @@ public static class ResourcesKey public static string About => _instance?.About ?? throw new InvalidOperationException("ResourcesKey.About is not initialized."); public static string ReadMore => _instance?.ReadMore ?? throw new InvalidOperationException("ResourcesKey.ReadMore is not initialized."); public static string ReadLess => _instance?.ReadLess ?? throw new InvalidOperationException("ResourcesKey.ReadLess is not initialized."); + public static string Detail => _instance?.Detail ?? throw new InvalidOperationException("ResourcesKey.Detail is not initialized."); + public static string Edit => _instance?.Edit ?? throw new InvalidOperationException("ResourcesKey.Edit is not initialized."); + public static string Delete => _instance?.Delete ?? throw new InvalidOperationException("ResourcesKey.Delete is not initialized."); } \ No newline at end of file diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Files/GameIdeas.fr.json b/src/GameIdeas/Server/GameIdeas.WebAPI/Files/GameIdeas.fr.json index 7507151..631c233 100644 --- a/src/GameIdeas/Server/GameIdeas.WebAPI/Files/GameIdeas.fr.json +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Files/GameIdeas.fr.json @@ -1,67 +1,73 @@ { - "GamesIdeas": "Game Ideas", - "ManualAdd": "Manuel", - "AutoAdd": "Automatique", - "Login": "Se connecter", - "Logout": "Se déconnecter", - "EnterUsername": "Nom d'utilisateur", - "EnterPassword": "Mot de passe", - "UserManager": "Gestion des utilisateurs", - "CategoriesManager": "Gestion des catégories", - "Filters": "Les filtres", - "LastAdd": "Les ajouts récents", - "Research": "Rechercher", - "Platforms": "Plateformes", - "Tags": "Genres", - "Publisher": "Editeur", - "Developer": "Développeur", - "StorageSize": "Taille d'espace", - "StorageSizeMo": "Taille d'espace en Mo", - "LastModification": "Dernière modifications", - "ReleaseDate": "Date de parution", - "CreateDate": "Date de création", - "UpdateDate": "Date de modification", - "Title": "Titre", - "Interest": "Intérêt", - "Properties": "Propriétés", - "Description": "Description", - "Save": "Enregister", - "Reset": "Annuler", - "ErrorWhenPostingData": "Erreur lors de la requête POST", - "ErrorWhenPutingData": "Erreur lors de la requête PUT", - "ErrorWhenDeletingData": "Erreur lors de la requête DELETE", - "ErrorWhenFetchingData": "Erreur lors de la requête GET", - "RequestFailedStatusFormat": "Erreur lors de la réponse, code {0}", - "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", - "Unknown": "Inconnu", - "ErrorFetchGames": "Erreur lors de la récupération des jeux", - "Ascending": "Ascendant", - "Descending": "Descendant", - "ErrorStorageSpaceLabel": "Erreur lors de la génération des label de l'espace de stockage", - "MinStorageSpaceFormat": "Jusqu'à {0}", - "MaxStorageSpaceFormat": "Plus de {0}", - "MinMaxStorageSpaceFormat": "{0} à {1}", - "UserArgumentsNull": "Nom d'utilisateur ou mot de passe invalide", - "InvalidToken": "Le token JWT est invalide", - "UserUnauthorized": "Utilisateur non authorisé", - "UserLoginFailed": "Authentification de l'utilisateur échoué", - "UserLogoutFailed": "Déconnection de l'utilisateur échoué", - "Roles": "Rôles", - "ErrorFetchUsers": "Erreur lors de la récupération des utilisateurs", - "ErrorFetchRoles": "Erreur lors de la récupération des rôles", - "MissingField": "Un champs est manquant", - "ErrorCreateUser": "Erreur lors de la création d'un utilisateur", - "ErrorUpdateUser": "Erreur lors de la mise à jour d'un utilisateur", - "ErrorDeleteUser": "Erreur lors de la suppression d'un utilisateur", - "Cancel": "Annuler", - "Confirm": "Confirmer", - "ConfirmDeleteDescription": "Êtes-vous sur de vouloir supprimer cet élément ?", - "Informations": "Informations", - "About": "À propos", - "ReadMore": "Afficher", - "ReadLess": "Réduire" + "GamesIdeas": "Game Ideas", + "ManualAdd": "Manuel", + "AutoAdd": "Automatique", + "Login": "Se connecter", + "Logout": "Se déconnecter", + "EnterUsername": "Nom d'utilisateur", + "EnterPassword": "Mot de passe", + "UserManager": "Gestion des utilisateurs", + "CategoriesManager": "Gestion des catégories", + "Filters": "Les filtres", + "LastAdd": "Les ajouts récents", + "Research": "Rechercher", + "Platforms": "Plateformes", + "Tags": "Genres", + "Publisher": "Editeur", + "Developer": "Développeur", + "StorageSize": "Taille d'espace", + "StorageSizeMo": "Taille d'espace en Mo", + "LastModification": "Dernière modifications", + "ReleaseDate": "Date de parution", + "CreateDate": "Date de création", + "UpdateDate": "Date de modification", + "Title": "Titre", + "Interest": "Intérêt", + "Properties": "Propriétés", + "Description": "Description", + "Save": "Enregister", + "Reset": "Annuler", + "ErrorWhenPostingData": "Erreur lors de la requête POST", + "ErrorWhenPutingData": "Erreur lors de la requête PUT", + "ErrorWhenDeletingData": "Erreur lors de la requête DELETE", + "ErrorWhenFetchingData": "Erreur lors de la requête GET", + "RequestFailedStatusFormat": "Erreur lors de la réponse, code {0}", + "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", + "ErrorDeleteGame": "Erreur lors de la suppression d'un jeu", + "ErrorUpdateGame": "Erreur lors de la modification d'un jeu", + "InvalidTitle": "Le titre est incorrect", + "InvalidInterest": "L'interêt est incorrect", + "Unknown": "Inconnu", + "ErrorFetchGames": "Erreur lors de la récupération des jeux", + "ErrorFetchDetail": "Erreur lors de la récupération des détails d'un jeu", + "Ascending": "Ascendant", + "Descending": "Descendant", + "ErrorStorageSpaceLabel": "Erreur lors de la génération des label de l'espace de stockage", + "MinStorageSpaceFormat": "Jusqu'à {0}", + "MaxStorageSpaceFormat": "Plus de {0}", + "MinMaxStorageSpaceFormat": "{0} à {1}", + "UserArgumentsNull": "Nom d'utilisateur ou mot de passe invalide", + "InvalidToken": "Le token JWT est invalide", + "UserUnauthorized": "Utilisateur non authorisé", + "UserLoginFailed": "Authentification de l'utilisateur échoué", + "UserLogoutFailed": "Déconnection de l'utilisateur échoué", + "Roles": "Rôles", + "ErrorFetchUsers": "Erreur lors de la récupération des utilisateurs", + "ErrorFetchRoles": "Erreur lors de la récupération des rôles", + "MissingField": "Un champs est manquant", + "ErrorCreateUser": "Erreur lors de la création d'un utilisateur", + "ErrorUpdateUser": "Erreur lors de la mise à jour d'un utilisateur", + "ErrorDeleteUser": "Erreur lors de la suppression d'un utilisateur", + "Cancel": "Annuler", + "Confirm": "Confirmer", + "ConfirmDeleteDescription": "Êtes-vous sur de vouloir supprimer cet élément ?", + "Informations": "Informations", + "About": "À propos", + "ReadMore": "Afficher", + "ReadLess": "Réduire", + "Detail": "Détail", + "Edit": "Modifier", + "Delete": "Supprimer" } \ No newline at end of file diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameWriteService.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameWriteService.cs index 4681b75..af144ee 100644 --- a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameWriteService.cs +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameWriteService.cs @@ -4,6 +4,7 @@ using GameIdeas.Shared.Exceptions; using GameIdeas.Shared.Model; using GameIdeas.WebAPI.Context; using Microsoft.EntityFrameworkCore; +using System.Threading.Tasks; namespace GameIdeas.WebAPI.Services.Games; @@ -13,7 +14,7 @@ public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameW { var gameToCreate = mapper.Map(gameDto); - HandleDeveloperPublisherCreation(gameToCreate); + await HandleDeveloperPublisherCreation(gameToCreate); await context.Games.AddAsync(gameToCreate); await context.SaveChangesAsync(); @@ -35,7 +36,7 @@ public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameW var gameToUpdate = mapper.Map(gameDto); - HandleDeveloperPublisherCreation(gameToUpdate); + await HandleDeveloperPublisherCreation(gameToUpdate); await HandlePlatformsCreation(gameDto.Platforms, gameToUpdate.Id); await HandlePropertiesCreation(gameDto.Properties, gameToUpdate.Id); await HandleTagsCreation(gameDto.Tags, gameToUpdate.Id); @@ -48,12 +49,27 @@ public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameW public async Task DeleteGame(int gameId) { + await HandlePlatformsCreation([], gameId); + await HandlePropertiesCreation([], gameId); + await HandleTagsCreation([], gameId); + var gameToRemove = await context.Games .FirstOrDefaultAsync(g => g.Id == gameId) ?? throw new NotFoundException($"[{typeof(Game).FullName}] with ID {gameId} has not been found in context"); context.Games.Remove(gameToRemove); - return await context.SaveChangesAsync() != 0; + await context.SaveChangesAsync(); + + context.Publishers.RemoveRange( + context.Publishers.Include(p => p.Games) + .Where(p => p.Games.Count == 0)); + + context.Developers.RemoveRange( + context.Developers.Include(d => d.Games) + .Where(d => d.Games.Count == 0)); + + await context.SaveChangesAsync(); + return true; } private async Task HandlePlatformsCreation(IEnumerable? categoriesToCreate, int gameId) @@ -62,6 +78,9 @@ public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameW { var gps = mapper.Map>(categoriesToCreate); + context.GamePlatforms.RemoveRange( + context.GamePlatforms.Where(gp => gp.GameId == gameId)); + foreach (var gp in gps) { gp.GameId = gameId; @@ -69,6 +88,14 @@ public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameW context.Platforms.AttachRange(gps.Select(gp => gp.Platform)); await context.GamePlatforms.AddRangeAsync(gps); + + await context.SaveChangesAsync(); + + context.Platforms.RemoveRange( + context.Platforms.Include(p => p.GamePlatforms) + .Where(p => p.GamePlatforms.Count == 0)); + + await context.SaveChangesAsync(); } } @@ -78,6 +105,9 @@ public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameW { var gps = mapper.Map>(categoriesToCreate); + context.GameProperties.RemoveRange( + context.GameProperties.Where(gp => gp.GameId == gameId)); + foreach (var gp in gps) { gp.GameId = gameId; @@ -85,6 +115,14 @@ public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameW context.Properties.AttachRange(gps.Select(gp => gp.Property)); await context.GameProperties.AddRangeAsync(gps); + + await context.SaveChangesAsync(); + + context.Properties.RemoveRange( + context.Properties.Include(p => p.GameProperties) + .Where(p => p.GameProperties.Count == 0)); + + await context.SaveChangesAsync(); } } @@ -94,6 +132,9 @@ public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameW { var gts = mapper.Map>(categoriesToCreate); + context.GameTags.RemoveRange( + context.GameTags.Where(gt => gt.GameId == gameId)); + foreach (var gt in gts) { gt.GameId = gameId; @@ -101,11 +142,30 @@ public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameW context.Tags.AttachRange(gts.Select(gt => gt.Tag)); await context.GameTags.AddRangeAsync(gts); + + await context.SaveChangesAsync(); + + context.Tags.RemoveRange( + context.Tags.Include(t => t.GameTags) + .Where(t => t.GameTags.Count == 0)); + + await context.SaveChangesAsync(); + } } - private void HandleDeveloperPublisherCreation(Game? game) + private async Task HandleDeveloperPublisherCreation(Game? game) { + context.Publishers.RemoveRange( + context.Publishers.Include(p => p.Games) + .Where(p => p.Games.Count == 0)); + + context.Developers.RemoveRange( + context.Developers.Include(d => d.Games) + .Where(d => d.Games.Count == 0)); + + await context.SaveChangesAsync(); + if (game?.Publisher != null) { context.Publishers.Attach(game.Publisher);