diff --git a/.github/workflows/pkg.yaml b/.github/workflows/pkg.yaml index e053a8c3..bdc85fb7 100644 --- a/.github/workflows/pkg.yaml +++ b/.github/workflows/pkg.yaml @@ -28,7 +28,7 @@ jobs: permissions: id-token: write contents: read - uses: devpro/github-workflow-parts/.github/workflows/reusable-container-publication.yml@62dbf6e833e49230ab34ef3c44093ebb727a095f + uses: devpro/github-workflow-parts/.github/workflows/reusable-container-publication.yml@4f3152777635eb3bcf1ee21db103d70f790ff1cb with: create-latest: ${{ github.ref_name == 'main' }} image-definition: ${{ matrix.image-definition }} diff --git a/Directory.Packages.props b/Directory.Packages.props index a9d925a0..068affa9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,25 +4,25 @@ true - + - - - - - - - + + + + + + + - + diff --git a/README.md b/README.md index 73dd74d2..f54abab1 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [](https://sonarcloud.io/dashboard?id=devpro_keeptrack) [](https://sonarcloud.io/dashboard?id=devpro_keeptrack) [](https://hub.docker.com/r/devprofr/keeptrack-blazorapp) + [](https://app.fossa.com/projects/custom%2B60068%2Fgithub.com%2Fdevpro%2Fkeeptrack?ref=badge_shield&issueType=license) [](https://app.fossa.com/projects/custom%2B60068%2Fgithub.com%2Fdevpro%2Fkeeptrack?ref=badge_shield&issueType=security) diff --git a/src/BlazorApp/Components/Inventory/Clients/MoviesApiClient.cs b/src/BlazorApp/Components/Inventory/Clients/MovieApiClient.cs similarity index 80% rename from src/BlazorApp/Components/Inventory/Clients/MoviesApiClient.cs rename to src/BlazorApp/Components/Inventory/Clients/MovieApiClient.cs index e1b12b6f..2773d99c 100644 --- a/src/BlazorApp/Components/Inventory/Clients/MoviesApiClient.cs +++ b/src/BlazorApp/Components/Inventory/Clients/MovieApiClient.cs @@ -2,7 +2,7 @@ namespace Keeptrack.BlazorApp.Components.Inventory.Clients; -public sealed class MoviesApiClient(HttpClient http) +public sealed class MovieApiClient(HttpClient http) : InventoryApiClientBase(http) { protected override string ApiResourceName => "/api/movies"; diff --git a/src/BlazorApp/Components/Inventory/Clients/MusicAlbumApiClient.cs b/src/BlazorApp/Components/Inventory/Clients/MusicAlbumApiClient.cs new file mode 100644 index 00000000..9b7e5db7 --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Clients/MusicAlbumApiClient.cs @@ -0,0 +1,9 @@ +using Keeptrack.WebApi.Contracts.Dto; + +namespace Keeptrack.BlazorApp.Components.Inventory.Clients; + +public sealed class MusicAlbumApiClient(HttpClient http) + : InventoryApiClientBase(http) +{ + protected override string ApiResourceName => "/api/movies"; +} diff --git a/src/BlazorApp/Components/Inventory/Pages/Books.razor b/src/BlazorApp/Components/Inventory/Pages/Books.razor index 4770787c..c22b772c 100644 --- a/src/BlazorApp/Components/Inventory/Pages/Books.razor +++ b/src/BlazorApp/Components/Inventory/Pages/Books.razor @@ -31,24 +31,51 @@ Title Author Series + Rating Finished at @book.Title @book.Author @book.Series - @book.FinishedAt?.ToString("yyyy-MM-dd") + + @book.FirstReadAt?.ToString("yyyy-MM-dd") - + + - - - - - - + + + Title + + + + Author + + + + Series + + + + Genre + + + + Rating + + + + Notes + + + + First read at + + + diff --git a/src/BlazorApp/Components/Inventory/Pages/Books.razor.cs b/src/BlazorApp/Components/Inventory/Pages/Books.razor.cs index 1d224898..18779d50 100644 --- a/src/BlazorApp/Components/Inventory/Pages/Books.razor.cs +++ b/src/BlazorApp/Components/Inventory/Pages/Books.razor.cs @@ -14,7 +14,10 @@ public partial class Books : InventoryPageBase Id = item.Id, Title = item.Title, Author = item.Author, - FinishedAt = item.FinishedAt, - Series = item.Series + Series = item.Series, + Genre = item.Genre, + Rating = item.Rating, + Notes = item.Notes, + FirstReadAt = item.FirstReadAt }; } diff --git a/src/BlazorApp/Components/Inventory/Pages/Movies.razor b/src/BlazorApp/Components/Inventory/Pages/Movies.razor index 180091a7..5363faed 100644 --- a/src/BlazorApp/Components/Inventory/Pages/Movies.razor +++ b/src/BlazorApp/Components/Inventory/Pages/Movies.razor @@ -30,54 +30,46 @@ Title Year - Genre Rating - Notes + First seen @movie.Title - @(movie.Year > 0 ? movie.Year : "—") - @(string.IsNullOrEmpty(movie.Genre) ? "—" : movie.Genre) - - @if (movie.Rating > 0) - { - - @(new string('★', movie.Rating ?? 0))@(new string('★', 5 - (movie.Rating ?? 0))) - - } - else - { - — - } - - @(string.IsNullOrEmpty(movie.Notes) ? "—" : movie.Notes) + @(movie.Year > 0 ? movie.Year : "") + + @movie.FirstSeenAt?.ToString("yyyy-MM-dd") - + - - — - @for (var i = 1; i <= 5; i++) - { - @(new string('★', i)) - } - - + + - - - - - - - — - @for (var i = 1; i <= 5; i++) - { - @(new string('★', i)) - } - - - - + + + Title + + + + Year + + + + Genre + + + + Rating + + + + Notes + + + + First seen at + + + diff --git a/src/BlazorApp/Components/Inventory/Pages/Movies.razor.cs b/src/BlazorApp/Components/Inventory/Pages/Movies.razor.cs index 494c5dca..e60a55aa 100644 --- a/src/BlazorApp/Components/Inventory/Pages/Movies.razor.cs +++ b/src/BlazorApp/Components/Inventory/Pages/Movies.razor.cs @@ -5,7 +5,7 @@ namespace Keeptrack.BlazorApp.Components.Inventory.Pages; public partial class Movies : InventoryPageBase { - [Inject] private MoviesApiClient MovieApi { get; set; } = null!; + [Inject] private MovieApiClient MovieApi { get; set; } = null!; protected override InventoryApiClientBase Api => MovieApi; @@ -16,6 +16,9 @@ public partial class Movies : InventoryPageBase Year = item.Year, Genre = item.Genre, Rating = item.Rating, - Notes = item.Notes + Notes = item.Notes, + FirstSeenAt = item.FirstSeenAt, + AllocineId = item.AllocineId, + ImdbPageId = item.ImdbPageId }; } diff --git a/src/BlazorApp/Components/Inventory/Pages/MusicAlbums.razor b/src/BlazorApp/Components/Inventory/Pages/MusicAlbums.razor new file mode 100644 index 00000000..026a0278 --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Pages/MusicAlbums.razor @@ -0,0 +1,67 @@ +@page "/music-albums" +@inherits InventoryPageBase +@rendermode InteractiveServer +@attribute [Authorize] + + + + Title + Artist + Rating + + + @album.Title + @album.Artist + + + + + + + + + + Title + + + + Artist + + + + Year + + + + Genre + + + + Rating + + + + diff --git a/src/BlazorApp/Components/Inventory/Pages/MusicAlbums.razor.cs b/src/BlazorApp/Components/Inventory/Pages/MusicAlbums.razor.cs new file mode 100644 index 00000000..8ff0ed0b --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Pages/MusicAlbums.razor.cs @@ -0,0 +1,21 @@ +using Keeptrack.WebApi.Contracts.Dto; +using Microsoft.AspNetCore.Components; + +namespace Keeptrack.BlazorApp.Components.Inventory.Pages; + +public partial class MusicAlbums : InventoryPageBase +{ + [Inject] private MusicAlbumApiClient MusicAlbumApi { get; set; } = null!; + + protected override InventoryApiClientBase Api => MusicAlbumApi; + + protected override MusicAlbumDto CloneItem(MusicAlbumDto item) => new() + { + Id = item.Id, + Title = item.Title, + Artist = item.Artist, + Genre = item.Genre, + Year = item.Year, + Rating = item.Rating + }; +} diff --git a/src/BlazorApp/Components/Inventory/Pages/TvShows.razor b/src/BlazorApp/Components/Inventory/Pages/TvShows.razor index e95c7132..c963dbab 100644 --- a/src/BlazorApp/Components/Inventory/Pages/TvShows.razor +++ b/src/BlazorApp/Components/Inventory/Pages/TvShows.razor @@ -4,7 +4,7 @@ @attribute [Authorize] Title + Last episode seen + Rating - - @movie.Title + + @show.Title + @show.LastEpisodeSeen + - - + + + + - - - + + + Title + + + + Year + + + + Rating + + + + Last episode seen + + + + Finished at + + + + Notes + + + diff --git a/src/BlazorApp/Components/Inventory/Pages/TvShows.razor.cs b/src/BlazorApp/Components/Inventory/Pages/TvShows.razor.cs index 02985a07..471c5e09 100644 --- a/src/BlazorApp/Components/Inventory/Pages/TvShows.razor.cs +++ b/src/BlazorApp/Components/Inventory/Pages/TvShows.razor.cs @@ -12,6 +12,13 @@ public partial class TvShows : InventoryPageBase protected override TvShowDto CloneItem(TvShowDto item) => new() { Id = item.Id, - Title = item.Title + Title = item.Title, + Rating = item.Rating, + AllocineId = item.AllocineId, + ImdbPageId = item.ImdbPageId, + Notes = item.Notes, + FinishedAt = item.FinishedAt, + LastEpisodeSeen = item.LastEpisodeSeen, + Year = item.Year }; } diff --git a/src/BlazorApp/Components/Inventory/Pages/VideoGames.razor b/src/BlazorApp/Components/Inventory/Pages/VideoGames.razor index aa548099..721abfd2 100644 --- a/src/BlazorApp/Components/Inventory/Pages/VideoGames.razor +++ b/src/BlazorApp/Components/Inventory/Pages/VideoGames.razor @@ -4,7 +4,7 @@ @attribute [Authorize] Title Platform State - Released + Rating Finished @game.Title @game.Platform @game.State - @game.ReleasedAt?.ToString("yyyy-MM-dd") + @game.FinishedAt?.ToString("yyyy-MM-dd") @@ -63,12 +63,16 @@ Current On-hold - - + + - - - + + + Title + + + + Platform — Xbox Series X @@ -81,18 +85,33 @@ PS2 PS1 - - - - — + + + State + + — State — Available Completed To resume Current On-hold - - - - + + + Year + + + + Rating + + + + Finished at + + + + Notes + + + diff --git a/src/BlazorApp/Components/Inventory/Pages/VideoGames.razor.cs b/src/BlazorApp/Components/Inventory/Pages/VideoGames.razor.cs index c555a408..c5484b3f 100644 --- a/src/BlazorApp/Components/Inventory/Pages/VideoGames.razor.cs +++ b/src/BlazorApp/Components/Inventory/Pages/VideoGames.razor.cs @@ -14,8 +14,10 @@ public partial class VideoGames : InventoryPageBase Id = item.Id, Title = item.Title, Platform = item.Platform, - ReleasedAt = item.ReleasedAt, State = item.State, - FinishedAt = item.FinishedAt + FinishedAt = item.FinishedAt, + Notes = item.Notes, + Rating = item.Rating, + Year = item.Year }; } diff --git a/src/BlazorApp/Components/Inventory/Shared/InventoryList.razor b/src/BlazorApp/Components/Inventory/Shared/InventoryList.razor index e9d43d8e..7824411d 100644 --- a/src/BlazorApp/Components/Inventory/Shared/InventoryList.razor +++ b/src/BlazorApp/Components/Inventory/Shared/InventoryList.razor @@ -5,10 +5,31 @@ @Error } +@if (_showModal) +{ + + + + Edit item + ✕ + + + + @EditModalTemplate(InlineForm) + + + + + +} + @if (ShowForm) { - @(Form.Id is null ? "New item" : "Edit item") + New item @FormTemplate(Form) @@ -60,30 +81,15 @@ else @foreach (var item in Items) { - @if (EditingInline?.Id == item.Id) - { - - @InlineEditTemplate(InlineForm) - - - Save - ✕ - - - - } - else - { - - @RowTemplate(item) - - - OnStartInlineEdit.InvokeAsync(item)">Edit - OnDelete.InvokeAsync(item.Id!)">Del - - - - } + + @RowTemplate(item) + + + OpenEditModal(item)">Edit + OnDelete.InvokeAsync(item.Id!)">Del + + + } @@ -99,14 +105,47 @@ else @if (TotalPages > 1) { - OnGoToPage.InvokeAsync(Page - 1)">← - @for (var p = 1; p <= TotalPages; p++) + OnGoToPage.InvokeAsync(1)">⇤ + + OnGoToPage.InvokeAsync(Page - 1)">← + + @{ + int start, end; + if (TotalPages <= 3) + { + start = 1; + end = TotalPages; + } + else if (Page <= 2) + { + start = 1; + end = 3; + } + else if (Page >= TotalPages - 1) + { + start = TotalPages - 2; + end = TotalPages; + } + else + { + start = Page - 1; + end = Page + 1; + } + } + @for (var p = start; p <= end; p++) { var pageNum = p; OnGoToPage.InvokeAsync(pageNum)">@pageNum } - OnGoToPage.InvokeAsync(Page + 1)">→ + OnGoToPage.InvokeAsync(Page + 1)">→ + + OnGoToPage.InvokeAsync(TotalPages)">⇥ + @TotalCount results } @@ -116,6 +155,8 @@ else @code { private bool _initialized; private string _localSearch = ""; + private bool _showModal; + private bool _mouseDownOnBackdrop; protected override void OnParametersSet() { @@ -137,6 +178,39 @@ else await OnClearSearch.InvokeAsync(); } + private async Task OpenEditModal(TDto item) + { + await OnStartInlineEdit.InvokeAsync(item); + _showModal = true; + } + + private async Task OnSaveModal() + { + await OnSaveInline.InvokeAsync(); + _showModal = false; + } + + private async Task OnCancelModal() + { + _showModal = false; + await OnCancelInline.InvokeAsync(); + } + + private void HandleBackdropMouseDown() + { + _mouseDownOnBackdrop = true; + } + + private async Task HandleBackdropClick() + { + if (_mouseDownOnBackdrop) + { + _mouseDownOnBackdrop = false; + await OnCancelModal(); + } + _mouseDownOnBackdrop = false; + } + [Parameter] public required string Title { get; set; } [Parameter] public required List Items { get; set; } [Parameter] public required TDto Form { get; set; } @@ -152,7 +226,7 @@ else [Parameter] public required RenderFragment HeaderTemplate { get; set; } [Parameter] public required RenderFragment RowTemplate { get; set; } [Parameter] public required RenderFragment FormTemplate { get; set; } - [Parameter] public required RenderFragment InlineEditTemplate { get; set; } + [Parameter] public required RenderFragment EditModalTemplate { get; set; } [Parameter] public required EventCallback OnSave { get; set; } [Parameter] public required EventCallback OnCancelForm { get; set; } [Parameter] public required EventCallback OnShowAddForm { get; set; } diff --git a/src/BlazorApp/Components/Inventory/Shared/StarRating.razor b/src/BlazorApp/Components/Inventory/Shared/StarRating.razor new file mode 100644 index 00000000..af9979db --- /dev/null +++ b/src/BlazorApp/Components/Inventory/Shared/StarRating.razor @@ -0,0 +1,15 @@ +@for (var i = 1; i <= 5; i++) +{ + ★ +} + +@code { + [Parameter] public float? Rating { get; set; } + + private string GetStarClass(int position) + { + var rating = Rating ?? 0; + if (rating >= position) return "full"; + return rating >= position - 0.5f ? "half" : "empty"; + } +} diff --git a/src/BlazorApp/Components/Layout/NavMenu.razor b/src/BlazorApp/Components/Layout/NavMenu.razor index 7e8e6535..66a22a47 100644 --- a/src/BlazorApp/Components/Layout/NavMenu.razor +++ b/src/BlazorApp/Components/Layout/NavMenu.razor @@ -24,6 +24,11 @@ 🎬 Movies + + + 🎵 Music albums + + 📺 TV shows diff --git a/src/BlazorApp/DependencyInjection/InfrastructureServiceCollectionExtensions.cs b/src/BlazorApp/DependencyInjection/InfrastructureServiceCollectionExtensions.cs index 9d999f6b..dbae8441 100644 --- a/src/BlazorApp/DependencyInjection/InfrastructureServiceCollectionExtensions.cs +++ b/src/BlazorApp/DependencyInjection/InfrastructureServiceCollectionExtensions.cs @@ -7,7 +7,9 @@ internal static void AddWebApiHttpClient(this IServiceCollection services, strin var webApiUri = new Uri(webApiBaseUrl); services.AddHttpClient(client => client.BaseAddress = webApiUri) .AddHttpMessageHandler(); - services.AddHttpClient(client => client.BaseAddress = webApiUri) + services.AddHttpClient(client => client.BaseAddress = webApiUri) + .AddHttpMessageHandler(); + services.AddHttpClient(client => client.BaseAddress = webApiUri) .AddHttpMessageHandler(); services.AddHttpClient(client => client.BaseAddress = webApiUri) .AddHttpMessageHandler(); diff --git a/src/BlazorApp/wwwroot/app.css b/src/BlazorApp/wwwroot/app.css index e211887e..ba460824 100644 --- a/src/BlazorApp/wwwroot/app.css +++ b/src/BlazorApp/wwwroot/app.css @@ -557,6 +557,15 @@ button.nav-link:hover { color: #e07070 !important; background: rgba(192,72,90,0. gap: 0.3rem; padding: 1rem 1.5rem; border-top: 1px solid var(--kt-border); + flex-wrap: wrap; +} + +@media (max-width: 767px) { + .kt-page-btn { + min-width: 30px; + height: 30px; + font-size: 0.75rem; + } } .kt-page-btn { @@ -600,3 +609,111 @@ button.nav-link:hover { color: #e07070 !important; background: rgba(192,72,90,0. font-size: 0.78rem; color: var(--kt-text-subtle); } + +/* ── Stars ────────────────────────────────────────────────────── */ +.star { font-size: 1.2rem; } +.star.full { color: gold; } +.star.empty { color: lightgray; } +.star.half { + position: relative; + display: inline-block; + color: lightgray; +} +.star.half::before { + content: '★'; + position: absolute; + left: 0; + width: 50%; + overflow: hidden; + color: gold; +} + +/* ── Modal ─────────────────────────────────────────────────────────── */ +.kt-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(3px); + z-index: 500; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + animation: kt-fade-in 0.15s ease; +} + +.kt-modal { + background: var(--kt-surface); + border: 1px solid var(--kt-border); + border-top: 2px solid var(--kt-amber); + border-radius: var(--kt-radius-lg); + box-shadow: var(--kt-shadow-lg); + width: 100%; + max-width: 520px; + max-height: 90vh; + display: flex; + flex-direction: column; + animation: kt-slide-up 0.18s cubic-bezier(0.4, 0, 0.2, 1); +} + +.kt-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.25rem 1.5rem 1rem; + border-bottom: 1px solid var(--kt-border); + flex-shrink: 0; +} + +.kt-modal-header h5 { + font-family: var(--kt-font-display); + font-style: italic; + color: var(--kt-amber); + font-size: 0.9rem; + font-weight: 600; + margin: 0; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.kt-modal-close { + background: none; + border: none; + color: var(--kt-text-muted); + font-size: 0.9rem; + cursor: pointer; + padding: 0.25rem 0.4rem; + border-radius: 5px; + line-height: 1; + transition: all var(--kt-transition); +} +.kt-modal-close:hover { color: var(--kt-text); background: var(--kt-surface-3); } + +.kt-modal-body { + padding: 1.5rem; + overflow-y: auto; + flex: 1; +} + +.kt-modal-footer { + display: flex; + justify-content: flex-end; + gap: 0.6rem; + padding: 1rem 1.5rem; + border-top: 1px solid var(--kt-border); + flex-shrink: 0; +} + +@keyframes kt-fade-in { from { opacity: 0; } to { opacity: 1; } } +@keyframes kt-slide-up { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } + +/* Mobile: slide up from bottom as a sheet */ +@media (max-width: 767px) { + .kt-modal-backdrop { align-items: flex-end; padding: 0; } + .kt-modal { + max-width: 100%; + max-height: 95vh; + border-radius: var(--kt-radius-lg) var(--kt-radius-lg) 0 0; + border-top: 2px solid var(--kt-amber); + } +} diff --git a/src/Domain/Models/BookModel.cs b/src/Domain/Models/BookModel.cs index d27c7692..f710772d 100644 --- a/src/Domain/Models/BookModel.cs +++ b/src/Domain/Models/BookModel.cs @@ -15,5 +15,11 @@ public class BookModel : IHasIdAndOwnerId public string? Series { get; set; } - public DateOnly? FinishedAt { get; set; } + public float? Rating { get; set; } + + public string? Genre { get; set; } + + public string? Notes { get; set; } + + public DateOnly? FirstReadAt { get; set; } } diff --git a/src/Domain/Models/MovieModel.cs b/src/Domain/Models/MovieModel.cs index a950f8b4..aa84358c 100644 --- a/src/Domain/Models/MovieModel.cs +++ b/src/Domain/Models/MovieModel.cs @@ -1,4 +1,5 @@ -using Keeptrack.Common.System; +using System; +using Keeptrack.Common.System; namespace Keeptrack.Domain.Models; @@ -12,7 +13,7 @@ public class MovieModel : IHasIdAndOwnerId public int? Year { get; set; } - public int? Rating { get; set; } + public float? Rating { get; set; } public string? Genre { get; set; } @@ -21,4 +22,6 @@ public class MovieModel : IHasIdAndOwnerId public string? ImdbPageId { get; set; } public string? AllocineId { get; set; } + + public DateOnly? FirstSeenAt { get; set; } } diff --git a/src/Domain/Models/MusicAlbumModel.cs b/src/Domain/Models/MusicAlbumModel.cs new file mode 100644 index 00000000..66d15c5b --- /dev/null +++ b/src/Domain/Models/MusicAlbumModel.cs @@ -0,0 +1,20 @@ +using Keeptrack.Common.System; + +namespace Keeptrack.Domain.Models; + +public class MusicAlbumModel : IHasIdAndOwnerId +{ + public string? Id { get; set; } + + public required string OwnerId { get; set; } + + public required string Title { get; set; } + + public required string Artist { get; set; } + + public int? Year { get; set; } + + public string? Genre { get; set; } + + public float? Rating { get; set; } +} diff --git a/src/Domain/Models/TvShowModel.cs b/src/Domain/Models/TvShowModel.cs index bb747d52..0a50131d 100644 --- a/src/Domain/Models/TvShowModel.cs +++ b/src/Domain/Models/TvShowModel.cs @@ -1,4 +1,5 @@ -using Keeptrack.Common.System; +using System; +using Keeptrack.Common.System; namespace Keeptrack.Domain.Models; @@ -9,4 +10,18 @@ public class TvShowModel : IHasIdAndOwnerId public required string OwnerId { get; set; } public required string Title { get; set; } + + public int? Year { get; set; } + + public float? Rating { get; set; } + + public string? Notes { get; set; } + + public string? LastEpisodeSeen { get; set; } + + public string? ImdbPageId { get; set; } + + public string? AllocineId { get; set; } + + public DateOnly? FinishedAt { get; set; } } diff --git a/src/Domain/Models/VideoGameModel.cs b/src/Domain/Models/VideoGameModel.cs index 497395ce..c23d5445 100644 --- a/src/Domain/Models/VideoGameModel.cs +++ b/src/Domain/Models/VideoGameModel.cs @@ -13,9 +13,13 @@ public class VideoGameModel : IHasIdAndOwnerId public required string Platform { get; set; } - public DateOnly? ReleasedAt { get; set; } - public required string State { get; set; } + public int? Year { get; set; } + + public float? Rating { get; set; } + + public string? Notes { get; set; } + public DateOnly? FinishedAt { get; set; } } diff --git a/src/Domain/Repositories/IMusicAlbumRepository.cs b/src/Domain/Repositories/IMusicAlbumRepository.cs new file mode 100644 index 00000000..d88f5072 --- /dev/null +++ b/src/Domain/Repositories/IMusicAlbumRepository.cs @@ -0,0 +1,5 @@ +using Keeptrack.Domain.Models; + +namespace Keeptrack.Domain.Repositories; + +public interface IMusicAlbumRepository : IDataRepository; diff --git a/src/Infrastructure.MongoDb/Entities/Book.cs b/src/Infrastructure.MongoDb/Entities/Book.cs index b58fb3ec..d9e05067 100644 --- a/src/Infrastructure.MongoDb/Entities/Book.cs +++ b/src/Infrastructure.MongoDb/Entities/Book.cs @@ -20,6 +20,12 @@ public class Book : IHasIdAndOwnerId public string? Series { get; set; } - [BsonElement("finished_at")] - public DateTime? FinishedAt { get; set; } + public float? Rating { get; set; } + + public string? Genre { get; set; } + + public string? Notes { get; set; } + + [BsonElement("first_read_at")] + public DateTime? FirstReadAt { get; set; } } diff --git a/src/Infrastructure.MongoDb/Entities/Movie.cs b/src/Infrastructure.MongoDb/Entities/Movie.cs index 7792fd10..628ef7ac 100644 --- a/src/Infrastructure.MongoDb/Entities/Movie.cs +++ b/src/Infrastructure.MongoDb/Entities/Movie.cs @@ -1,4 +1,5 @@ -using Keeptrack.Common.System; +using System; +using Keeptrack.Common.System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; @@ -17,7 +18,7 @@ public class Movie : IHasIdAndOwnerId public int? Year { get; set; } - public int? Rating { get; set; } + public float? Rating { get; set; } public string? Genre { get; set; } @@ -26,4 +27,7 @@ public class Movie : IHasIdAndOwnerId public Imdb? Imdb { get; set; } public Allocine? Allocine { get; set; } + + [BsonElement("first_seen_at")] + public DateTime? FirstSeenAt { get; set; } } diff --git a/src/Infrastructure.MongoDb/Entities/MusicAlbum.cs b/src/Infrastructure.MongoDb/Entities/MusicAlbum.cs new file mode 100644 index 00000000..301a99aa --- /dev/null +++ b/src/Infrastructure.MongoDb/Entities/MusicAlbum.cs @@ -0,0 +1,25 @@ +using Keeptrack.Common.System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Keeptrack.Infrastructure.MongoDb.Entities; + +public class MusicAlbum : IHasIdAndOwnerId +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + + [BsonElement("owner_id")] + public required string OwnerId { get; set; } + + public required string Title { get; set; } + + public required string Artist { get; set; } + + public int? Year { get; set; } + + public string? Genre { get; set; } + + public float? Rating { get; set; } +} diff --git a/src/Infrastructure.MongoDb/Entities/TvShow.cs b/src/Infrastructure.MongoDb/Entities/TvShow.cs index 3e5917cd..6d69f612 100644 --- a/src/Infrastructure.MongoDb/Entities/TvShow.cs +++ b/src/Infrastructure.MongoDb/Entities/TvShow.cs @@ -1,4 +1,5 @@ -using Keeptrack.Common.System; +using System; +using Keeptrack.Common.System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; @@ -14,4 +15,20 @@ public class TvShow : IHasIdAndOwnerId public required string OwnerId { get; set; } public required string Title { get; set; } + + public int? Year { get; set; } + + public float? Rating { get; set; } + + public string? Notes { get; set; } + + [BsonElement("last_episode_seen")] + public string? LastEpisodeSeen { get; set; } + + public Imdb? Imdb { get; set; } + + public Allocine? Allocine { get; set; } + + [BsonElement("finished_at")] + public DateTime? FinishedAt { get; set; } } diff --git a/src/Infrastructure.MongoDb/Entities/VideoGame.cs b/src/Infrastructure.MongoDb/Entities/VideoGame.cs index 59f81b7e..5f3e136e 100644 --- a/src/Infrastructure.MongoDb/Entities/VideoGame.cs +++ b/src/Infrastructure.MongoDb/Entities/VideoGame.cs @@ -20,9 +20,12 @@ public class VideoGame : IHasIdAndOwnerId public required string State { get; set; } + public int? Year { get; set; } + + public float? Rating { get; set; } + + public string? Notes { get; set; } + [BsonElement("finished_at")] public DateTime? FinishedAt { get; set; } - - [BsonElement("released_at")] - public DateTime? ReleasedAt { get; set; } } diff --git a/src/Infrastructure.MongoDb/Repositories/MusicAlbumRepository.cs b/src/Infrastructure.MongoDb/Repositories/MusicAlbumRepository.cs new file mode 100644 index 00000000..a03e6bec --- /dev/null +++ b/src/Infrastructure.MongoDb/Repositories/MusicAlbumRepository.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Keeptrack.Infrastructure.MongoDb.Entities; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Keeptrack.Infrastructure.MongoDb.Repositories; + +public class MusicAlbumRepository(IMongoDatabase mongoDatabase, ILogger> logger, IMapper mapper) + : MongoDbRepositoryBase(mongoDatabase, logger, mapper), IMusicAlbumRepository +{ + protected override string CollectionName => "music-album"; + + protected override FilterDefinition GetFilter(string ownerId, string? search, MusicAlbumModel input) + { + var builder = Builders.Filter; + var filter = builder.Eq(f => f.OwnerId, ownerId); + if (!string.IsNullOrEmpty(search)) builder.Where(f => f.Title.Contains(search, System.StringComparison.CurrentCultureIgnoreCase) + || f.Artist.Contains(search, System.StringComparison.CurrentCultureIgnoreCase)); + return filter; + } +} + diff --git a/src/WebApi.Contracts/Dto/BookDto.cs b/src/WebApi.Contracts/Dto/BookDto.cs index 0519b383..eaff23ee 100644 --- a/src/WebApi.Contracts/Dto/BookDto.cs +++ b/src/WebApi.Contracts/Dto/BookDto.cs @@ -31,8 +31,14 @@ public class BookDto : IHasId /// Middle-earth Universe public string? Series { get; set; } + public float? Rating { get; set; } + + public string? Genre { get; set; } + + public string? Notes { get; set; } + /// /// Book finished reading date. /// - public DateOnly? FinishedAt { get; set; } + public DateOnly? FirstReadAt { get; set; } } diff --git a/src/WebApi.Contracts/Dto/MovieDto.cs b/src/WebApi.Contracts/Dto/MovieDto.cs index 7069d272..dd82b6f7 100644 --- a/src/WebApi.Contracts/Dto/MovieDto.cs +++ b/src/WebApi.Contracts/Dto/MovieDto.cs @@ -1,4 +1,5 @@ -using Keeptrack.Common.System; +using System; +using Keeptrack.Common.System; namespace Keeptrack.WebApi.Contracts.Dto; @@ -10,7 +11,7 @@ public class MovieDto : IHasId public int? Year { get; set; } - public int? Rating { get; set; } + public float? Rating { get; set; } public string? Genre { get; set; } @@ -19,4 +20,6 @@ public class MovieDto : IHasId public string? ImdbPageId { get; set; } public string? AllocineId { get; set; } + + public DateOnly? FirstSeenAt { get; set; } } diff --git a/src/WebApi.Contracts/Dto/MusicAlbumDto.cs b/src/WebApi.Contracts/Dto/MusicAlbumDto.cs new file mode 100644 index 00000000..bf33e892 --- /dev/null +++ b/src/WebApi.Contracts/Dto/MusicAlbumDto.cs @@ -0,0 +1,18 @@ +using Keeptrack.Common.System; + +namespace Keeptrack.WebApi.Contracts.Dto; + +public class MusicAlbumDto : IHasId +{ + public string? Id { get; set; } + + public string? Title { get; set; } + + public string? Artist { get; set; } + + public int? Year { get; set; } + + public string? Genre { get; set; } + + public float? Rating { get; set; } +} diff --git a/src/WebApi.Contracts/Dto/TvShowDto.cs b/src/WebApi.Contracts/Dto/TvShowDto.cs index bab686bd..8191fcdc 100644 --- a/src/WebApi.Contracts/Dto/TvShowDto.cs +++ b/src/WebApi.Contracts/Dto/TvShowDto.cs @@ -1,4 +1,5 @@ -using Keeptrack.Common.System; +using System; +using Keeptrack.Common.System; namespace Keeptrack.WebApi.Contracts.Dto; @@ -16,4 +17,18 @@ public class TvShowDto : IHasId /// TV Show title. /// public string? Title { get; set; } + + public int? Year { get; set; } + + public float? Rating { get; set; } + + public string? Notes { get; set; } + + public string? LastEpisodeSeen { get; set; } + + public string? ImdbPageId { get; set; } + + public string? AllocineId { get; set; } + + public DateOnly? FinishedAt { get; set; } } diff --git a/src/WebApi.Contracts/Dto/VideoGameDto.cs b/src/WebApi.Contracts/Dto/VideoGameDto.cs index c2416411..3b466922 100644 --- a/src/WebApi.Contracts/Dto/VideoGameDto.cs +++ b/src/WebApi.Contracts/Dto/VideoGameDto.cs @@ -23,16 +23,17 @@ public class VideoGameDto : IHasId /// public string? Platform { get; set; } - /// - /// Released date. - /// - public DateOnly? ReleasedAt { get; set; } - /// /// Current payling state. /// public string? State { get; set; } + public int? Year { get; set; } + + public float? Rating { get; set; } + + public string? Notes { get; set; } + /// /// Finished date. /// diff --git a/src/WebApi/Controllers/MusicAlbumController.cs b/src/WebApi/Controllers/MusicAlbumController.cs new file mode 100644 index 00000000..dd476f78 --- /dev/null +++ b/src/WebApi/Controllers/MusicAlbumController.cs @@ -0,0 +1,12 @@ +using Keeptrack.Domain.Models; +using Keeptrack.Domain.Repositories; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Keeptrack.WebApi.Controllers; + +[ApiController] +[Authorize] +[Route("api/music-albums")] +public class MusicAlbumController(IMapper mapper, IMusicAlbumRepository dataRepository) + : DataCrudControllerBase(mapper, dataRepository); diff --git a/src/WebApi/MappingProfiles/DataStorageMappingProfile.cs b/src/WebApi/MappingProfiles/DataStorageMappingProfile.cs index c380cafb..acf92a28 100644 --- a/src/WebApi/MappingProfiles/DataStorageMappingProfile.cs +++ b/src/WebApi/MappingProfiles/DataStorageMappingProfile.cs @@ -12,8 +12,32 @@ public DataStorageMappingProfile() CreateMap(); CreateMap(); - CreateMap(); - CreateMap(); + CreateMap() + .ForMember(x => x.AllocineId, opt => opt.MapFrom( + x => x.Allocine != null ? x.Allocine.Id : null)) + .ForMember(x => x.ImdbPageId, opt => opt.MapFrom( + x => x.Imdb != null ? x.Imdb.PageId : null)); + + CreateMap() + .ForMember(x => x.Allocine, opt => opt.MapFrom( + x => !string.IsNullOrEmpty(x.AllocineId) ? new Infrastructure.MongoDb.Entities.Allocine { Id = x.AllocineId } : null)) + .ForMember(x => x.Imdb, opt => opt.MapFrom( + x => !string.IsNullOrEmpty(x.ImdbPageId) ? new Infrastructure.MongoDb.Entities.Imdb { PageId = x.ImdbPageId } : null)); + + CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(x => x.AllocineId, opt => opt.MapFrom( + x => x.Allocine != null ? x.Allocine.Id : null)) + .ForMember(x => x.ImdbPageId, opt => opt.MapFrom( + x => x.Imdb != null ? x.Imdb.PageId : null)); + + CreateMap() + .ForMember(x => x.Allocine, opt => opt.MapFrom( + x => !string.IsNullOrEmpty(x.AllocineId) ? new Infrastructure.MongoDb.Entities.Allocine { Id = x.AllocineId } : null)) + .ForMember(x => x.Imdb, opt => opt.MapFrom( + x => !string.IsNullOrEmpty(x.ImdbPageId) ? new Infrastructure.MongoDb.Entities.Imdb { PageId = x.ImdbPageId } : null)); CreateMap(); CreateMap(); diff --git a/src/WebApi/MappingProfiles/MovieDataStorageMappingProfile.cs b/src/WebApi/MappingProfiles/MovieDataStorageMappingProfile.cs deleted file mode 100644 index 1a3a50ef..00000000 --- a/src/WebApi/MappingProfiles/MovieDataStorageMappingProfile.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Keeptrack.WebApi.MappingProfiles; - -public class MovieDataStorageMappingProfile : Profile -{ - public override string ProfileName - { - get { return "KeeptrackMovieDataStorageMappingProfile"; } - } - - public MovieDataStorageMappingProfile() - { - CreateMap() - .ForMember(x => x.AllocineId, opt => opt.MapFrom( - x => x.Allocine != null ? x.Allocine.Id : null)) - .ForMember(x => x.ImdbPageId, opt => opt.MapFrom( - x => x.Imdb != null ? x.Imdb.PageId : null)); - - CreateMap() - .ForMember(x => x.Allocine, opt => opt.MapFrom( - x => !string.IsNullOrEmpty(x.AllocineId) ? new Infrastructure.MongoDb.Entities.Allocine { Id = x.AllocineId } : null)) - .ForMember(x => x.Imdb, opt => opt.MapFrom( - x => !string.IsNullOrEmpty(x.ImdbPageId) ? new Infrastructure.MongoDb.Entities.Imdb { PageId = x.ImdbPageId } : null)); - } -} diff --git a/src/WebApi/MappingProfiles/WebServiceMappingProfile.cs b/src/WebApi/MappingProfiles/WebServiceMappingProfile.cs index 7beba997..a0341fe0 100644 --- a/src/WebApi/MappingProfiles/WebServiceMappingProfile.cs +++ b/src/WebApi/MappingProfiles/WebServiceMappingProfile.cs @@ -9,17 +9,25 @@ public override string ProfileName public WebServiceMappingProfile() { - CreateMap() + CreateMap() .ForMember(x => x.OwnerId, opt => opt.Ignore()); - CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(x => x.OwnerId, opt => opt.Ignore()); + CreateMap(); CreateMap() .ForMember(x => x.OwnerId, opt => opt.Ignore()); CreateMap(); - CreateMap() + CreateMap() .ForMember(x => x.OwnerId, opt => opt.Ignore()); - CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(x => x.OwnerId, opt => opt.Ignore()); + CreateMap(); CreateMap() .ForMember(x => x.OwnerId, opt => opt.Ignore()); diff --git a/test/WebApi.IntegrationTests/Resources/BookResourceTest.cs b/test/WebApi.IntegrationTests/Resources/BookResourceTest.cs index 7d5070cb..57c101f2 100644 --- a/test/WebApi.IntegrationTests/Resources/BookResourceTest.cs +++ b/test/WebApi.IntegrationTests/Resources/BookResourceTest.cs @@ -36,7 +36,7 @@ public async Task BookResourceFullCycle_IsOk() await PutAsync($"/{ResourceEndpoint}/{created.Id}", created); var updated = await GetAsync($"/{ResourceEndpoint}/{created.Id}"); - updated.Should().BeEquivalentTo(created, x => x.Excluding(item => item.FinishedAt)); // issue with DateTime and MongoDB + updated.Should().BeEquivalentTo(created, x => x.Excluding(item => item.FirstReadAt)); // issue with DateTime and MongoDB var finalItems = await GetAsync>($"/{ResourceEndpoint}"); finalItems.TotalCount.Should().BeGreaterThan(initialItems.TotalCount);