Skip to content

Commit

Permalink
feat: Basic working playlist page
Browse files Browse the repository at this point in the history
  • Loading branch information
data-miner00 committed Jun 25, 2024
1 parent a924fc5 commit 4b0aa9f
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 8 deletions.
12 changes: 12 additions & 0 deletions src/Linker.Core.V2/ApiModels/CreatePlaylistRequest.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
51 changes: 51 additions & 0 deletions src/Linker.Core.V2/Repositories/IPlaylistRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace Linker.Core.V2.Repositories;

using Linker.Core.V2.Models;
using System.Collections.Generic;
using System.Threading.Tasks;

/// <summary>
/// The abstraction for the <see cref="Playlist"/> repository.
/// </summary>
public interface IPlaylistRepository
{
/// <summary>
/// Gets all playlists for a certain user.
/// </summary>
/// <param name="userId">The user ID.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The list of playlists.</returns>
Task<IEnumerable<Playlist>> GetAllByUserAsync(string userId, CancellationToken cancellationToken);

/// <summary>
/// Gets the playlist by Id.
/// </summary>
/// <param name="id">The playlist Id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The found playlist.</returns>
Task<Playlist> GetByIdAsync(string id, CancellationToken cancellationToken);

/// <summary>
/// Creates a new playlist.
/// </summary>
/// <param name="playlist">The playlist to be created.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task.</returns>
Task AddAsync(Playlist playlist, CancellationToken cancellationToken);

/// <summary>
/// Updates an existing playlist.
/// </summary>
/// <param name="playlist">The playlist to be updated.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task.</returns>
Task UpdateAsync(Playlist playlist, CancellationToken cancellationToken);

/// <summary>
/// Removes a playlist by Id.
/// </summary>
/// <param name="id">The id of the playlist to be removed.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task.</returns>
Task RemoveAsync(string id, CancellationToken cancellationToken);
}
123 changes: 123 additions & 0 deletions src/Linker.Data/SqlServer/PlaylistRepository.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// The repository layer for <see cref="Playlist"/>.
/// </summary>
public sealed class PlaylistRepository : IPlaylistRepository
{
private readonly IDbConnection connection;

/// <summary>
/// Initializes a new instance of the <see cref="PlaylistRepository"/> class.
/// </summary>
/// <param name="connection">The database connection.</param>
public PlaylistRepository(IDbConnection connection)
{
this.connection = Guard.ThrowIfNull(connection);
}

/// <inheritdoc/>
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,
});
}

/// <inheritdoc/>
public Task<IEnumerable<Playlist>> GetAllByUserAsync(string userId, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var query = "SELECT * FROM [dbo].[Playlists] WHERE [OwnerId] = @UserId;";

return this.connection.QueryAsync<Playlist>(query, new { UserId = userId });
}

/// <inheritdoc/>
public Task<Playlist> GetByIdAsync(string id, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var query = "SELECT * FROM [dbo].[Playlists] WHERE [Id] = @Id;";

return this.connection.QueryFirstAsync<Playlist>(query, new { Id = id });
}

/// <inheritdoc/>
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 });
}

/// <inheritdoc/>
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,
});
}
}
74 changes: 71 additions & 3 deletions src/Linker.Mvc/Controllers/PlaylistController.cs
Original file line number Diff line number Diff line change
@@ -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<IActionResult> 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<IActionResult> 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);
}
}
}
3 changes: 2 additions & 1 deletion src/Linker.Mvc/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ private static WebApplicationBuilder ConfigureRepositories(this WebApplicationBu
builder.Services
.AddSingleton<ILinkRepository, LinkRepository>()
.AddSingleton<IUserRepository, UserRepository>()
.AddSingleton<IWorkspaceRepository, WorkspaceRepository>();
.AddSingleton<IWorkspaceRepository, WorkspaceRepository>()
.AddSingleton<IPlaylistRepository, PlaylistRepository>();

return builder;
}
Expand Down
33 changes: 33 additions & 0 deletions src/Linker.Mvc/Views/Playlist/Create.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@using Linker.Core.V2.Models
@model Linker.Core.V2.ApiModels.CreatePlaylistRequest
@{
ViewData["Title"] = "Create playlist";
}

<form asp-action="Create" class="max-w-screen-md">
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>

<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<input asp-for="Description" class="form-control" />
<span asp-validation-for="Description" class="text-danger"></span>
</div>

<div class="form-group">
<label asp-for="Visibility" class="control-label"></label>
<select asp-for="Visibility" asp-items="Html.GetEnumSelectList<Visibility>()">
<option selected="selected" value="">Please select</option>
</select>
<span asp-validation-for="Visibility" class="text-danger"></span>
</div>

<div class="my-3 flex">
<button class="px-4 py-2 bg-black text-white rounded block">
Create
</button>
</div>
</form>
31 changes: 27 additions & 4 deletions src/Linker.Mvc/Views/Playlist/Index.cshtml
Original file line number Diff line number Diff line change
@@ -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<Playlist>
@{
ViewData["Title"] = "Playlist";
var playlistCounts = Model.Count();
}

<partial name="_Notification" />

<a asp-action="Create">+</a>

@if (Model.Any())
{
@foreach (var playlist in Model)
{
<div>
<p>@playlist.Id</p>
<p>@playlist.Name</p>
<p>@playlist.Description</p>
<p>@playlist.Visibility.ToString()</p>
<p>@playlist.CreatedAt.ToLongTimeString()</p>
<p>@playlist.ModifiedAt.ToLongTimeString()</p>
</div>
}
}
else
{
<p>No playlist for now.</p>
<button>Create a new playlist.</button>
}

<p>Stub Playlist Page</p>

0 comments on commit 4b0aa9f

Please sign in to comment.