feat: adding reading list

This commit is contained in:
Oliver Booth 2023-12-14 16:03:24 +00:00
parent 3186ce9b50
commit 5cb61d275d
Signed by: oliverbooth
GPG Key ID: E60B570D1B7557B5
9 changed files with 244 additions and 0 deletions

View File

@ -0,0 +1,19 @@
namespace OliverBooth.Data.Web;
/// <summary>
/// Represents a book.
/// </summary>
internal sealed class Book : IBook
{
/// <inheritdoc />
public string Author { get; }
/// <inheritdoc />
public string Isbn { get; }
/// <inheritdoc />
public BookState State { get; }
/// <inheritdoc />
public string Title { get; }
}

View File

@ -0,0 +1,22 @@
namespace OliverBooth.Data.Web;
/// <summary>
/// Represents the state of a book.
/// </summary>
public enum BookState
{
/// <summary>
/// The book has been read and finished.
/// </summary>
Read,
/// <summary>
/// The book is on the current reading list.
/// </summary>
Reading,
/// <summary>
/// The book is on a future reading list.
/// </summary>
PlanToRead
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace OliverBooth.Data.Web.Configuration;
/// <summary>
/// Represents the configuration for the <see cref="Book" /> entity.
/// </summary>
internal sealed class BookConfiguration : IEntityTypeConfiguration<Book>
{
/// <inheritdoc />
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("Book");
builder.HasKey(entry => entry.Isbn);
builder.Property(entry => entry.Isbn).IsRequired();
builder.Property(entry => entry.Title).IsRequired();
builder.Property(entry => entry.Author).IsRequired();
builder.Property(entry => entry.State).HasConversion<EnumToStringConverter<BookState>>().IsRequired();
}
}

View File

@ -0,0 +1,31 @@
namespace OliverBooth.Data.Web;
/// <summary>
/// Represents a book.
/// </summary>
public interface IBook
{
/// <summary>
/// Gets the author of the book.
/// </summary>
/// <value>The author of the book.</value>
string Author { get; }
/// <summary>
/// Gets the ISBN of the book.
/// </summary>
/// <value>The ISBN of the book.</value>
string Isbn { get; }
/// <summary>
/// Gets the state of the book.
/// </summary>
/// <value>The state of the book.</value>
BookState State { get; }
/// <summary>
/// Gets the title of the book.
/// </summary>
/// <value>The title of the book.</value>
string Title { get; }
}

View File

@ -19,6 +19,12 @@ internal sealed class WebContext : DbContext
_configuration = configuration; _configuration = configuration;
} }
/// <summary>
/// Gets the collection of books in the reading list.
/// </summary>
/// <value>The collection of books.</value>
public DbSet<Book> Books { get; private set; } = null!;
/// <summary> /// <summary>
/// Gets the collection of projects in the database. /// Gets the collection of projects in the database.
/// </summary> /// </summary>
@ -47,6 +53,7 @@ internal sealed class WebContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.ApplyConfiguration(new BookConfiguration());
modelBuilder.ApplyConfiguration(new ProjectConfiguration()); modelBuilder.ApplyConfiguration(new ProjectConfiguration());
modelBuilder.ApplyConfiguration(new TemplateConfiguration()); modelBuilder.ApplyConfiguration(new TemplateConfiguration());
modelBuilder.ApplyConfiguration(new SiteConfigurationConfiguration()); modelBuilder.ApplyConfiguration(new SiteConfigurationConfiguration());

View File

@ -0,0 +1,90 @@
@page
@using OliverBooth.Data.Web
@model OliverBooth.Pages.Books
@{
ViewData["Title"] = "Reading List";
}
<h1 class="display-4">Reading List</h1>
<p>
This is a list of the books I've read, I'm currently reading, or that I plan to read. Not every book is listed here,
but I will update this list as I try to remember what it is I've read in the past.
</p>
<p>
This list is also available on <a href="https://www.goodreads.com/review/list/145592619">Goodreads</a>.
</p>
<p class="lead">Currently Reading</p>
<table class="table reading-list">
<thead>
<tr>
<th style="width: 50%">Title</th>
<th style="width: 25%">Author</th>
<th style="width: 25%">ISBN</th>
</tr>
</thead>
<tbody>
@foreach (IBook book in Model.CurrentlyReading.OrderBy(b => b.Author).ThenBy(b => b.Title))
{
<tr>
<td>
<img class="book-cover" src="https://cdn.olivr.me/img/books/@(book.Isbn.Trim()).jpg" alt="Book Cover">
@book.Title.Trim()
</td>
<td>@book.Author.Trim()</td>
<td>@book.Isbn.Trim()</td>
</tr>
}
</tbody>
</table>
<p class="lead">Plan to Read</p>
<table class="table reading-list">
<thead>
<tr>
<th style="width: 50%">Title</th>
<th style="width: 25%">Author</th>
<th style="width: 25%">ISBN</th>
</tr>
</thead>
<tbody>
@foreach (IBook book in Model.PlanToRead.OrderBy(b => b.Author).ThenBy(b => b.Title))
{
<tr>
<td>
<img class="book-cover" src="https://cdn.olivr.me/img/books/@(book.Isbn.Trim()).jpg" alt="Book Cover">
@book.Title.Trim()
</td>
<td>@book.Author.Trim()</td>
<td>@book.Isbn.Trim()</td>
</tr>
}
</tbody>
</table>
<p class="lead">Read</p>
<table class="table reading-list">
<thead>
<tr>
<th style="width: 50%">Title</th>
<th style="width: 25%">Author</th>
<th style="width: 25%">ISBN</th>
</tr>
</thead>
<tbody>
@foreach (IBook book in Model.Read.OrderBy(b => b.Author).ThenBy(b => b.Title))
{
<tr>
<td>
<img class="book-cover" src="https://cdn.olivr.me/img/books/@(book.Isbn.Trim()).jpg" alt="Book Cover">
@book.Title.Trim()
</td>
<td>@book.Author.Trim()</td>
<td>@book.Isbn.Trim()</td>
</tr>
}
</tbody>
</table>

View File

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using OliverBooth.Data.Web;
using OliverBooth.Services;
namespace OliverBooth.Pages;
public class Books : PageModel
{
private readonly IReadingListService _readingListService;
public Books(IReadingListService readingListService)
{
_readingListService = readingListService;
}
/// <summary>
/// Gets the books currently being read.
/// </summary>
/// <value>The books currently being read.</value>
public IReadOnlyCollection<IBook> CurrentlyReading { get; private set; } = ArraySegment<IBook>.Empty;
/// <summary>
/// Gets the books that are planned to be read.
/// </summary>
/// <value>The books that are planned to be read.</value>
public IReadOnlyCollection<IBook> PlanToRead { get; private set; } = ArraySegment<IBook>.Empty;
/// <summary>
/// Gets the books that have been read.
/// </summary>
/// <value>The books that have been read.</value>
public IReadOnlyCollection<IBook> Read { get; private set; } = ArraySegment<IBook>.Empty;
public void OnGet()
{
CurrentlyReading = _readingListService.GetBooks(BookState.Reading);
PlanToRead = _readingListService.GetBooks(BookState.PlanToRead);
Read = _readingListService.GetBooks(BookState.Read);
}
}

View File

@ -36,6 +36,7 @@ builder.Services.AddSingleton<ITemplateService, TemplateService>();
builder.Services.AddSingleton<IBlogPostService, BlogPostService>(); builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
builder.Services.AddSingleton<IBlogUserService, BlogUserService>(); builder.Services.AddSingleton<IBlogUserService, BlogUserService>();
builder.Services.AddSingleton<IProjectService, ProjectService>(); builder.Services.AddSingleton<IProjectService, ProjectService>();
builder.Services.AddSingleton<IReadingListService, ReadingListService>();
builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddRouting(options => options.LowercaseUrls = true); builder.Services.AddRouting(options => options.LowercaseUrls = true);

View File

@ -301,3 +301,14 @@ a.brand-linkedin {
color: #fff; color: #fff;
} }
} }
table.reading-list {
th,td {
vertical-align: middle;
}
}
.book-cover {
width: 50px;
vertical-align: middle;
}