diff --git a/src/Linker.Core.V2/ApiModels/CreatePlaylistRequest.cs b/src/Linker.Core.V2/ApiModels/CreatePlaylistRequest.cs new file mode 100644 index 0000000..01c71da --- /dev/null +++ b/src/Linker.Core.V2/ApiModels/CreatePlaylistRequest.cs @@ -0,0 +1,12 @@ +namespace Linker.Core.V2.ApiModels; + +using Linker.Core.V2.Models; + +public sealed class CreatePlaylistRequest +{ + public string Name { get; set; } + + public string Description { get; set; } + + public Visibility Visibility { get; set; } +} diff --git a/src/Linker.Core.V2/Repositories/IPlaylistRepository.cs b/src/Linker.Core.V2/Repositories/IPlaylistRepository.cs new file mode 100644 index 0000000..aec4a82 --- /dev/null +++ b/src/Linker.Core.V2/Repositories/IPlaylistRepository.cs @@ -0,0 +1,51 @@ +namespace Linker.Core.V2.Repositories; + +using Linker.Core.V2.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +/// +/// The abstraction for the repository. +/// +public interface IPlaylistRepository +{ + /// + /// Gets all playlists for a certain user. + /// + /// The user ID. + /// The cancellation token. + /// The list of playlists. + Task> GetAllByUserAsync(string userId, CancellationToken cancellationToken); + + /// + /// Gets the playlist by Id. + /// + /// The playlist Id. + /// The cancellation token. + /// The found playlist. + Task GetByIdAsync(string id, CancellationToken cancellationToken); + + /// + /// Creates a new playlist. + /// + /// The playlist to be created. + /// The cancellation token. + /// The task. + Task AddAsync(Playlist playlist, CancellationToken cancellationToken); + + /// + /// Updates an existing playlist. + /// + /// The playlist to be updated. + /// The cancellation token. + /// The task. + Task UpdateAsync(Playlist playlist, CancellationToken cancellationToken); + + /// + /// Removes a playlist by Id. + /// + /// The id of the playlist to be removed. + /// The cancellation token. + /// The task. + Task RemoveAsync(string id, CancellationToken cancellationToken); +} diff --git a/src/Linker.Data/SqlServer/PlaylistRepository.cs b/src/Linker.Data/SqlServer/PlaylistRepository.cs new file mode 100644 index 0000000..c488e30 --- /dev/null +++ b/src/Linker.Data/SqlServer/PlaylistRepository.cs @@ -0,0 +1,123 @@ +namespace Linker.Data.SqlServer; + +using Dapper; +using Linker.Common.Helpers; +using Linker.Core.V2.Models; +using Linker.Core.V2.Repositories; +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading; +using System.Threading.Tasks; + +/// +/// The repository layer for . +/// +public sealed class PlaylistRepository : IPlaylistRepository +{ + private readonly IDbConnection connection; + + /// + /// Initializes a new instance of the class. + /// + /// The database connection. + public PlaylistRepository(IDbConnection connection) + { + this.connection = Guard.ThrowIfNull(connection); + } + + /// + public Task AddAsync(Playlist playlist, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var statement = @" + INSERT INTO [dbo].[Playlists] + ( + [Id], + [OwnerId], + [Name], + [Description], + [Visibility], + [CreatedAt], + [ModifiedAt] + ) + VALUES + ( + @Id, + @OwnerId, + @Name, + @Description, + @Visibility, + @CreatedAt, + @ModifiedAt + ); + "; + + return this.connection.ExecuteAsync(statement, new + { + playlist.Id, + playlist.OwnerId, + playlist.Name, + playlist.Description, + Visibility = playlist.Visibility.ToString(), + CreatedAt = DateTime.UtcNow, + ModifiedAt = DateTime.UtcNow, + }); + } + + /// + public Task> GetAllByUserAsync(string userId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var query = "SELECT * FROM [dbo].[Playlists] WHERE [OwnerId] = @UserId;"; + + return this.connection.QueryAsync(query, new { UserId = userId }); + } + + /// + public Task GetByIdAsync(string id, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var query = "SELECT * FROM [dbo].[Playlists] WHERE [Id] = @Id;"; + + return this.connection.QueryFirstAsync(query, new { Id = id }); + } + + /// + public Task RemoveAsync(string id, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var command = "DELETE FROM [dbo].[Playlists] WHERE [Id] = @Id;"; + + return this.connection.ExecuteAsync(command, new { Id = id }); + } + + /// + public Task UpdateAsync(Playlist playlist, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var command = @" + UPDATE [dbo].[Playlists] + SET + [Name] = @Name, + [Description] = @Description, + [Visibility] = @Visibility, + [ModifiedAt] = @ModifiedAt + WHERE [Id] = @Id; + "; + + return this.connection.ExecuteAsync(command, new + { + playlist.Id, + playlist.Name, + playlist.Description, + Visibility = playlist.Visibility.ToString(), + ModifiedAt = DateTime.UtcNow, + }); + } +} diff --git a/src/Linker.Mvc/Controllers/PlaylistController.cs b/src/Linker.Mvc/Controllers/PlaylistController.cs index 4b316a3..7faaa91 100644 --- a/src/Linker.Mvc/Controllers/PlaylistController.cs +++ b/src/Linker.Mvc/Controllers/PlaylistController.cs @@ -1,11 +1,79 @@ namespace Linker.Mvc.Controllers; +using Linker.Common.Helpers; +using Linker.Core.V2.ApiModels; +using Linker.Core.V2.Models; +using Linker.Core.V2.Repositories; using Microsoft.AspNetCore.Mvc; +using Serilog; +using System.Security.Claims; -public class PlaylistController : Controller +public sealed class PlaylistController : Controller { - public IActionResult Index() + private readonly IPlaylistRepository repository; + private readonly ILogger logger; + + public PlaylistController(IPlaylistRepository repository, ILogger logger) + { + this.repository = Guard.ThrowIfNull(repository); + this.logger = Guard.ThrowIfNull(logger); + } + + public string UserId => + this.HttpContext.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty; + + public async Task Index() { - return View(); + var playlists = await this.repository.GetAllByUserAsync(this.UserId, default); + + return this.View(playlists); + } + + // GET: PlaylistController/Create + public IActionResult Create() + { + return this.View(); + } + + // POST: PlaylistController/Create + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(CreatePlaylistRequest request) + { + ArgumentNullException.ThrowIfNull(request); + + var playlist = new Playlist + { + Id = Guid.NewGuid().ToString(), + OwnerId = this.UserId, + Name = request.Name, + Description = request.Description, + Visibility = request.Visibility, + }; + + try + { + if (this.ModelState.IsValid) + { + await this.repository + .AddAsync(playlist, this.HttpContext.RequestAborted) + .ConfigureAwait(false); + + this.TempData[Constants.Success] = "Playlist created successfully"; + + return this.RedirectToAction(nameof(this.Index)); + } + + this.logger.Warning("The model is invalid. {@model}.", this.ModelState); + + return this.View(request); + } + catch (Exception ex) + { + this.TempData[Constants.Error] = "Something failed: " + ex.Message; + this.logger.Error(ex, "Exception occurred."); + + return this.View(request); + } } } diff --git a/src/Linker.Mvc/Program.cs b/src/Linker.Mvc/Program.cs index d2fc182..700b779 100644 --- a/src/Linker.Mvc/Program.cs +++ b/src/Linker.Mvc/Program.cs @@ -90,7 +90,8 @@ private static WebApplicationBuilder ConfigureRepositories(this WebApplicationBu builder.Services .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); return builder; } diff --git a/src/Linker.Mvc/Views/Playlist/Create.cshtml b/src/Linker.Mvc/Views/Playlist/Create.cshtml new file mode 100644 index 0000000..94cb0da --- /dev/null +++ b/src/Linker.Mvc/Views/Playlist/Create.cshtml @@ -0,0 +1,33 @@ +@using Linker.Core.V2.Models +@model Linker.Core.V2.ApiModels.CreatePlaylistRequest +@{ + ViewData["Title"] = "Create playlist"; +} + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ +
+
diff --git a/src/Linker.Mvc/Views/Playlist/Index.cshtml b/src/Linker.Mvc/Views/Playlist/Index.cshtml index 9ded020..c43cc0e 100644 --- a/src/Linker.Mvc/Views/Playlist/Index.cshtml +++ b/src/Linker.Mvc/Views/Playlist/Index.cshtml @@ -1,8 +1,31 @@ -@* - For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 -*@ +@using Linker.Core.V2.Models +@model IEnumerable @{ ViewData["Title"] = "Playlist"; + var playlistCounts = Model.Count(); +} + + + ++ + +@if (Model.Any()) +{ + @foreach (var playlist in Model) + { +
+

@playlist.Id

+

@playlist.Name

+

@playlist.Description

+

@playlist.Visibility.ToString()

+

@playlist.CreatedAt.ToLongTimeString()

+

@playlist.ModifiedAt.ToLongTimeString()

+
+ } +} +else +{ +

No playlist for now.

+ } -

Stub Playlist Page