diff --git a/JSMR.Api/Startup/WebApplicationExtensions.cs b/JSMR.Api/Startup/WebApplicationExtensions.cs index b783e1a..356c35b 100644 --- a/JSMR.Api/Startup/WebApplicationExtensions.cs +++ b/JSMR.Api/Startup/WebApplicationExtensions.cs @@ -5,6 +5,8 @@ using JSMR.Application.Tags.Commands.SetEnglishName; using JSMR.Application.Tags.Commands.UpdateTagStatus; using JSMR.Application.Tags.Queries.Search; using JSMR.Application.Users; +using JSMR.Application.VoiceWorks.Commands.Delete; +using JSMR.Application.VoiceWorks.Commands.SetFavorite; using JSMR.Application.VoiceWorks.Queries.Search; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; @@ -59,6 +61,7 @@ public static class WebApplicationExtensions app.MapGet("/health", () => Results.Ok(new { status = "ok" })); app.MapSearchEndpoints(); + app.MapVoiceWorkCommandEndpoints(); app.MapTagCommandEndpoints(); app.MapCreatorCommandEndpoints(); app.MapAuthenticationEndpoints(); @@ -110,6 +113,27 @@ public static class WebApplicationExtensions }); } + private static void MapVoiceWorkCommandEndpoints(this WebApplication app) + { + app.MapPost("/api/voicework/set-favorite", async ( + SetVoiceWorkFavoriteRequest request, + SetVoiceWorkFavoriteHandler handler, + CancellationToken ct) => + { + var result = await handler.HandleAsync(request, ct); + return Results.Ok(result); + }); + + app.MapPost("/api/voicework/delete", async ( + DeleteVoiceWorkRequest request, + DeleteVoiceWorkHandler handler, + CancellationToken ct) => + { + var result = await handler.HandleAsync(request, ct); + return Results.Ok(result); + }); + } + private static void MapTagCommandEndpoints(this WebApplication app) { app.MapPost("/api/tags/update-status", async ( diff --git a/JSMR.Application/DI/ApplicationServiceCollectionExtensions.cs b/JSMR.Application/DI/ApplicationServiceCollectionExtensions.cs index 4853a91..2bb89fd 100644 --- a/JSMR.Application/DI/ApplicationServiceCollectionExtensions.cs +++ b/JSMR.Application/DI/ApplicationServiceCollectionExtensions.cs @@ -5,6 +5,8 @@ using JSMR.Application.Scanning; using JSMR.Application.Tags.Commands.SetEnglishName; using JSMR.Application.Tags.Commands.UpdateTagStatus; using JSMR.Application.Tags.Queries.Search; +using JSMR.Application.VoiceWorks.Commands.Delete; +using JSMR.Application.VoiceWorks.Commands.SetFavorite; using JSMR.Application.VoiceWorks.Queries.Search; using Microsoft.Extensions.DependencyInjection; @@ -18,6 +20,9 @@ public static class ApplicationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); diff --git a/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkHandler.cs b/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkHandler.cs index 922d855..2b4f1b9 100644 --- a/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkHandler.cs +++ b/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkHandler.cs @@ -2,7 +2,7 @@ namespace JSMR.Application.VoiceWorks.Commands.Delete; -public sealed class DeleteVoiceWorkFavoriteHandler(IVoiceWorkWriter writer) +public sealed class DeleteVoiceWorkHandler(IVoiceWorkWriter writer) { public async Task HandleAsync(DeleteVoiceWorkRequest request, CancellationToken cancellationToken = default) { diff --git a/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchResult.cs b/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchResult.cs index 8888173..89a06c9 100644 --- a/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchResult.cs +++ b/JSMR.Application/VoiceWorks/Queries/Search/VoiceWorkSearchResult.cs @@ -4,29 +4,29 @@ namespace JSMR.Application.VoiceWorks.Queries.Search; public record VoiceWorkSearchResult { - public int VoiceWorkId { get; init; } - public required string ProductId { get; init; } - public string? OriginalProductId { get; init; } - public string? Description { get; init; } - public required string ProductName { get; init; } - public required string ProductUrl { get; init; } - public bool HasImage { get; init; } - public required string Maker { get; init; } - public required string MakerId { get; init; } - public DateTime? ExpectedDate { get; init; } - public DateTime? SalesDate { get; init; } - public DateTime? PlannedReleaseDate { get; init; } - public int? Downloads { get; init; } - public int? WishlistCount { get; init; } - public byte? StarRating { get; init; } - public int? Votes { get; init; } - public bool HasTrial { get; init; } - public bool HasChobit { get; init; } - public AgeRating Rating { get; init; } - public bool Favorite { get; init; } - public byte Status { get; init; } - public byte SubtitleLanguage { get; init; } - public bool? IsValid { get; init; } + public int VoiceWorkId { get; set; } + public required string ProductId { get; set; } + public string? OriginalProductId { get; set; } + public string? Description { get; set; } + public required string ProductName { get; set; } + public required string ProductUrl { get; set; } + public bool HasImage { get; set; } + public required string Maker { get; set; } + public required string MakerId { get; set; } + public DateTime? ExpectedDate { get; set; } + public DateTime? SalesDate { get; set; } + public DateTime? PlannedReleaseDate { get; set; } + public int? Downloads { get; set; } + public int? WishlistCount { get; set; } + public byte? StarRating { get; set; } + public int? Votes { get; set; } + public bool HasTrial { get; set; } + public bool HasChobit { get; set; } + public AgeRating Rating { get; set; } + public bool Favorite { get; set; } + public byte Status { get; set; } + public byte SubtitleLanguage { get; set; } + public bool? IsValid { get; set; } public required VoiceWorkCircleItem Circle { get; set; } public VoiceWorkCircleItem? OriginalCircle { get; set; } public VoiceWorkTagItem[] Tags { get; set; } = []; diff --git a/JSMR.UI.Blazor/Components/Chip.razor b/JSMR.UI.Blazor/Components/Chip.razor index 186ea6c..806e794 100644 --- a/JSMR.UI.Blazor/Components/Chip.razor +++ b/JSMR.UI.Blazor/Components/Chip.razor @@ -11,7 +11,10 @@ UseCurrentColor> } - @ChildContent + @if (ChildContent is not null) + { + @ChildContent + } } else @@ -26,7 +29,10 @@ else UseCurrentColor> } - @ChildContent + @if (ChildContent is not null) + { + @ChildContent + } } @@ -62,6 +68,12 @@ else [Parameter] public EventCallback Click { get; set; } + [Parameter] + public bool IsClickable { get; set; } + + [Parameter] + public bool ThickBorder { get; set; } + private string GetClasses() { string color = Color.ToString().ToLower(); @@ -72,6 +84,17 @@ else $"color-{color}" ]; + // Experimental + if (ChildContent is null) + { + classNames.Add("j-chip-icon-only"); + } + + if (ThickBorder) + { + classNames.Add("j-chip-thick-border"); + } + switch (Varient) { case ElementVarient.Filled: @@ -99,7 +122,7 @@ else classNames.Add($"varient-tint"); } - if (Click.HasDelegate || string.IsNullOrWhiteSpace(Url) == false) + if (Click.HasDelegate || string.IsNullOrWhiteSpace(Url) == false || IsClickable) { classNames.Add("is-clickable"); } diff --git a/JSMR.UI.Blazor/Components/JProduct.razor b/JSMR.UI.Blazor/Components/JProduct.razor index 992d17f..8e5b586 100644 --- a/JSMR.UI.Blazor/Components/JProduct.razor +++ b/JSMR.UI.Blazor/Components/JProduct.razor @@ -1,4 +1,6 @@ -@using JSMR.Application.VoiceWorks.Queries.Search +@using AntDesign +@using JSMR.Application.VoiceWorks.Commands.SetFavorite +@using JSMR.Application.VoiceWorks.Queries.Search @using JSMR.Domain.Enums @using JSMR.UI.Blazor.Components.Chips @using JSMR.UI.Blazor.Enums @@ -7,6 +9,10 @@ @using System.Globalization @using Microsoft.AspNetCore.WebUtilities +@inject VoiceWorksClient Client +@inject MessageService MessageService +@inject ModalService ModalService +
@@ -16,12 +22,6 @@ @Product.ProductName
- @* @Product.Maker *@ - @* *@ @Product.Maker @if (Product.OriginalCircle is not null) @@ -31,12 +31,6 @@ @foreach (var creator in Product.Creators) { - @* @creator.Name *@ - @* *@ @creator.Name } @@ -70,16 +64,9 @@
@foreach (var tag in Product.Tags) { - @*
@tag.Name
*@ }
- @*
- @foreach (var tag in Product.Tags) - { - - } -
*@
@@ -102,20 +89,45 @@ @if (Product.IsValid != true) { - + @* *@ + } @if (Product.OriginalProductId is not null) { - + @* *@ + } @if (Product.Favorite) { - + @* *@ + } @if (Product.HasTrial || Product.HasChobit) { - + @* *@ + } + + + + @if (!Product.Favorite) + { + Add to Favorites + } + @if (Product.Favorite) + { + Remove from Favorites + } + @if (Product.IsValid != true) + { + Delete + } + + + + + +
@@ -194,4 +206,43 @@ default: return code; } } + + private async Task SetFavorite(bool value) + { + SetVoiceWorkFavoriteRequest request = new( + VoiceWorkId: Product.VoiceWorkId, + IsFavorite: value + ); + + SetVoiceWorkFavoriteResponse? response = await Client.SetVoiceWorkFavoriteeAsync(request); + + if (response is not null) + { + Product.Favorite = response.IsFavorite; + } + + //await InvokeAsync(StateHasChanged); + + MessageConfig messageConfig = new() + { + Content = $"Product '{Product.ProductName}' has been {(Product.Favorite ? "added to your favorites" : "removed from your favorites")}.", + Type = MessageType.Success + }; + + _ = MessageService.OpenAsync(messageConfig); + } + + private async Task Delete() + { + RenderFragment icon = @; + + AntDesign.ConfirmOptions options = new() + { + Title = "Are you sure you want to delete the following product?", + Icon = icon, + Content = Product.ProductName + }; + + await ModalService.ConfirmAsync(options); + } } diff --git a/JSMR.UI.Blazor/Enums/Graphic.cs b/JSMR.UI.Blazor/Enums/Graphic.cs index 2c8f101..dd0fc37 100644 --- a/JSMR.UI.Blazor/Enums/Graphic.cs +++ b/JSMR.UI.Blazor/Enums/Graphic.cs @@ -21,5 +21,6 @@ public enum Graphic Age, Calendar, Download, - Microphone + Microphone, + Pencil } \ No newline at end of file diff --git a/JSMR.UI.Blazor/Services/VoiceWorksClient.cs b/JSMR.UI.Blazor/Services/VoiceWorksClient.cs index 6c8aa1c..ca01d67 100644 --- a/JSMR.UI.Blazor/Services/VoiceWorksClient.cs +++ b/JSMR.UI.Blazor/Services/VoiceWorksClient.cs @@ -4,6 +4,8 @@ using JSMR.Application.Creators.Queries.Search; using JSMR.Application.Tags.Commands.SetEnglishName; using JSMR.Application.Tags.Commands.UpdateTagStatus; using JSMR.Application.Tags.Queries.Search; +using JSMR.Application.VoiceWorks.Commands.Delete; +using JSMR.Application.VoiceWorks.Commands.SetFavorite; using JSMR.Application.VoiceWorks.Queries.Search; using System.Net.Http.Json; using System.Text.Json; @@ -45,6 +47,18 @@ public class VoiceWorksClient(HttpClient http) return await resp.Content.ReadFromJsonAsync(JsonOptions, cancellationToken: ct); } + public async Task SetVoiceWorkFavoriteeAsync(SetVoiceWorkFavoriteRequest request, CancellationToken ct = default) + { + using var resp = await http.PostAsJsonAsync("/api/voicework/set-favorite", request, ct); + return await resp.Content.ReadFromJsonAsync(JsonOptions, cancellationToken: ct); + } + + public async Task DeleteVoiceWorkAsync(DeleteVoiceWorkRequest request, CancellationToken ct = default) + { + using var resp = await http.PostAsJsonAsync("/api/voicework/delete", request, ct); + return await resp.Content.ReadFromJsonAsync(JsonOptions, cancellationToken: ct); + } + public async Task UpdateTagStatusAsync(UpdateTagStatusRequest request, CancellationToken ct = default) { using var resp = await http.PostAsJsonAsync("/api/tags/update-status", request, ct); diff --git a/JSMR.UI.Blazor/wwwroot/css/ant-design.css b/JSMR.UI.Blazor/wwwroot/css/ant-design.css new file mode 100644 index 0000000..8ae7838 --- /dev/null +++ b/JSMR.UI.Blazor/wwwroot/css/ant-design.css @@ -0,0 +1,48 @@ +/* Modals */ +.ant-modal-content { + background-color: var(--ant-modal-content-bg); + border-radius: var(--ant-border-radius-lg); + border-width: 1px; + border-style: solid; + border-top-color: rgb(83, 99, 109); + border-left-color: rgb(72, 88, 99); + border-right-color: rgb(72, 88, 99); + border-bottom-color: rgb(63, 78, 88); +} + +.ant-modal-confirm-title { + color: var(--ant-modal-title-color); + text-shadow: 1px 1px 2px black; +} + +.ant-modal-confirm-body .ant-modal-confirm-content { + color: var(--ant-color-text); +} + +/* Buttons */ +.ant-btn { + font-weight: var(--ant-button-font-weight); + border: var(--ant-btn-border-width) var(--ant-btn-border-style) var(--ant-btn-border-color); + color: var(--ant-btn-text-color); + background-color: var(--ant-btn-bg-color); +} + +/* + --ant-button-font-weight: 400; + --ant-button-icon-gap: 8px; + --ant-button-padding-inline: 15px; + --ant-button-default-border-color: rgba(180, 200, 214, 0.25); + --ant-button-content-font-size: 14px; + --ant-btn-color-base: var(--ant-button-default-border-color); + --ant-btn-text-color: var(--ant-button-default-color); + --ant-btn-text-color-hover: var(--ant-button-default-hover-color); + --ant-btn-shadow: var(--ant-button-default-shadow); + --ant-btn-border-color: var(--ant-btn-color-base); + --ant-btn-border-color-hover: var(--ant-btn-color-hover); + --ant-btn-border-color-active: var(--ant-btn-color-active); + --ant-btn-bg-color: var(--ant-btn-bg-color-container); + --ant-btn-text-color: var(--ant-btn-color-base); + --ant-btn-text-color-hover: var(--ant-btn-color-hover); + --ant-btn-text-color-active: var(--ant-btn-color-active); + +*/ \ No newline at end of file diff --git a/JSMR.UI.Blazor/wwwroot/css/app.css b/JSMR.UI.Blazor/wwwroot/css/app.css index 2293dcc..232c03b 100644 --- a/JSMR.UI.Blazor/wwwroot/css/app.css +++ b/JSMR.UI.Blazor/wwwroot/css/app.css @@ -669,11 +669,21 @@ code { border: 1px solid transparent; } - .j-chip.is-clickable { - cursor: pointer; - user-select: none; - transition: background .2s linear, color .2s linear, border-color .2s linear, filter .2s linear; - } +.j-chip-icon-only { + padding: .75em; + border-radius: 2em; +} + +.j-chip-thick-border, +.j-chip.varient-outlined.j-chip-thick-border { + border-width: 2px; +} + +.j-chip.is-clickable { + cursor: pointer; + user-select: none; + transition: background .2s linear, color .2s linear, border-color .2s linear, filter .2s linear; +} .j-chip.is-clickable:hover { filter: brightness(1.2); @@ -770,6 +780,10 @@ code { --chip-fg-rgb: var(--rgb-on-yellow, 255 255 255); } +.j-chip.color-orange { + --chip-rgb: var(--rgb-orange); +} + .j-chip.color-pink { color: rgb(var(--chip-fg-rgb)); --chip-rgb: var(--rgb-pink); @@ -955,6 +969,14 @@ code { mask-image: url("../svg/microphone-fill.svg"); } +.j-icon-pencil { + mask-image: url("../svg/pencil.svg"); +} + +.j-icon-pencil-fill { + mask-image: url("../svg/pencil-fill.svg"); +} + .j-icon-2 { background-repeat: no-repeat; background-position: center; diff --git a/JSMR.UI.Blazor/wwwroot/css/theme-frozen.css b/JSMR.UI.Blazor/wwwroot/css/theme-frozen.css index b810123..776a4ae 100644 --- a/JSMR.UI.Blazor/wwwroot/css/theme-frozen.css +++ b/JSMR.UI.Blazor/wwwroot/css/theme-frozen.css @@ -48,6 +48,7 @@ --rgb-blue: 115 196 255; --rgb-yellow: 255 224 115; --rgb-on-yellow: 0 0 0; + --rgb-orange: 255 167 115; --rgb-pink: 224 104 148; --rgb-on-pink: 255 255 255; --rgb-red: 224 104 104; @@ -74,6 +75,44 @@ --surface-container-outline-high: rgb(83, 99, 109); --surface-container-outline: rgb(72, 88, 99); --surface-container-outline-low: rgb(63, 78, 88); + /* Ant Design - Modals */ + --ant-color-text: #b4c8d6; + --ant-modal-content-bg: #273f50; + --ant-modal-title-color: #b4c8d6; + /* Ant Design - Buttons */ + /* Button Part I */ + --ant-btn-text-color: var(--ant-button-default-color); + --ant-btn-text-color-hover: var(--ant-button-default-hover-color); + --ant-btn-text-color-active: var(--ant-button-default-active-color); + --ant-btn-bg-color-container: var(--ant-button-default-bg); + --ant-btn-bg-color-hover: var(--ant-button-default-hover-bg); + --ant-btn-bg-color-active: var(--ant-button-default-active-bg); + /* Part II */ + --ant-button-default-bg: #1e3545; + --ant-button-default-border-color: rgba(180, 200, 214, 0.25); + --ant-button-font-weight: 400; + --ant-button-icon-gap: 8px; + --ant-button-padding-inline: 15px; + --ant-button-content-font-size: 14px; + --ant-border-radius-lg: 16px; + --ant-button-font-weight: 400; + --ant-btn-border-width: var(--ant-line-width); + --ant-btn-border-color: #000; + --ant-btn-border-color-hover: var(--ant-btn-border-color); + --ant-btn-border-color-active: var(--ant-btn-border-color); + --ant-btn-border-color-disabled: var(--ant-btn-border-color); + --ant-btn-border-style: solid; + --ant-btn-text-color: #000; + --ant-btn-text-color-hover: var(--ant-btn-text-color); + --ant-btn-text-color-active: var(--ant-btn-text-color); + --ant-btn-text-color-disabled: var(--ant-btn-text-color); + --ant-btn-border-color: var(--ant-btn-color-base); + --ant-btn-border-color-hover: var(--ant-btn-color-hover); + --ant-btn-border-color-active: var(--ant-btn-color-active); + --ant-btn-bg-color: var(--ant-btn-bg-color-container); + --ant-btn-text-color: var(--ant-btn-color-base); + --ant-btn-text-color-hover: var(--ant-btn-color-hover); + --ant-btn-text-color-active: var(--ant-btn-color-active); } /* diff --git a/JSMR.UI.Blazor/wwwroot/index.html b/JSMR.UI.Blazor/wwwroot/index.html index 91fbdc1..091d096 100644 --- a/JSMR.UI.Blazor/wwwroot/index.html +++ b/JSMR.UI.Blazor/wwwroot/index.html @@ -24,6 +24,7 @@ + diff --git a/JSMR.UI.Blazor/wwwroot/svg/pencil-fill.svg b/JSMR.UI.Blazor/wwwroot/svg/pencil-fill.svg new file mode 100644 index 0000000..d0455a3 --- /dev/null +++ b/JSMR.UI.Blazor/wwwroot/svg/pencil-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/JSMR.UI.Blazor/wwwroot/svg/pencil.svg b/JSMR.UI.Blazor/wwwroot/svg/pencil.svg new file mode 100644 index 0000000..d033bff --- /dev/null +++ b/JSMR.UI.Blazor/wwwroot/svg/pencil.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file