Phase 13A: Book Metadata Backend.
This commit is contained in:
parent
57df736ad2
commit
4e71010814
@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
using PlotLine.ViewModels;
|
||||
|
||||
@ -30,8 +31,14 @@ public sealed class BooksController(IBookService books) : Controller
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Save(BookEditViewModel model)
|
||||
{
|
||||
if (!BookStatuses.IsValid(model.Status))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Status), "Select a valid book status.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await books.PopulateEditContextAsync(model);
|
||||
return View("Edit", model);
|
||||
}
|
||||
|
||||
@ -50,8 +57,17 @@ public sealed class BooksController(IBookService books) : Controller
|
||||
}
|
||||
|
||||
createModel.BookTitle = model.BookTitle;
|
||||
createModel.Subtitle = model.Subtitle;
|
||||
createModel.Tagline = model.Tagline;
|
||||
createModel.ShortDescription = model.ShortDescription;
|
||||
createModel.Status = model.Status;
|
||||
createModel.BookNumber = model.BookNumber;
|
||||
createModel.Description = model.Description;
|
||||
createModel.TargetWordCount = model.TargetWordCount;
|
||||
createModel.WritingStartDate = model.WritingStartDate;
|
||||
createModel.TargetCompletionDate = model.TargetCompletionDate;
|
||||
createModel.PublicationDate = model.PublicationDate;
|
||||
await books.PopulateEditContextAsync(createModel);
|
||||
return View("Edit", createModel);
|
||||
}
|
||||
|
||||
|
||||
@ -74,6 +74,30 @@ public sealed class ProjectBackupRepository(ISqlConnectionFactory connectionFact
|
||||
return null;
|
||||
}
|
||||
|
||||
var bookMetadata = (await connection.QueryAsync<Book>(
|
||||
"""
|
||||
SELECT BookID, Subtitle, Tagline, ShortDescription, Status, TargetWordCount, WritingStartDate, TargetCompletionDate, PublicationDate
|
||||
FROM dbo.Books
|
||||
WHERE ProjectID = @ProjectID;
|
||||
""",
|
||||
new { ProjectID = projectId })).ToDictionary(x => x.BookID);
|
||||
foreach (var book in data.Books)
|
||||
{
|
||||
if (!bookMetadata.TryGetValue(book.BookID, out var metadata))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
book.Subtitle = metadata.Subtitle;
|
||||
book.Tagline = metadata.Tagline;
|
||||
book.ShortDescription = metadata.ShortDescription;
|
||||
book.Status = metadata.Status;
|
||||
book.TargetWordCount = metadata.TargetWordCount;
|
||||
book.WritingStartDate = metadata.WritingStartDate;
|
||||
book.TargetCompletionDate = metadata.TargetCompletionDate;
|
||||
book.PublicationDate = metadata.PublicationDate;
|
||||
}
|
||||
|
||||
return new ProjectBackupExport
|
||||
{
|
||||
ExportedAtUtc = DateTime.UtcNow,
|
||||
|
||||
@ -2041,7 +2041,22 @@ public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IB
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.Book_Save",
|
||||
new { book.BookID, book.ProjectID, book.BookTitle, book.BookNumber, book.Description },
|
||||
new
|
||||
{
|
||||
book.BookID,
|
||||
book.ProjectID,
|
||||
book.BookTitle,
|
||||
book.Subtitle,
|
||||
book.Tagline,
|
||||
book.ShortDescription,
|
||||
book.Status,
|
||||
book.BookNumber,
|
||||
book.Description,
|
||||
book.TargetWordCount,
|
||||
book.WritingStartDate,
|
||||
book.TargetCompletionDate,
|
||||
book.PublicationDate
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
|
||||
@ -192,14 +192,60 @@ public sealed class Book
|
||||
public int BookID { get; set; }
|
||||
public int ProjectID { get; set; }
|
||||
public string BookTitle { get; set; } = string.Empty;
|
||||
public string? Subtitle { get; set; }
|
||||
public string? Tagline { get; set; }
|
||||
public string? ShortDescription { get; set; }
|
||||
public string Status { get; set; } = BookStatuses.Planning;
|
||||
public int BookNumber { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int? TargetWordCount { get; set; }
|
||||
public DateTime? WritingStartDate { get; set; }
|
||||
public DateTime? TargetCompletionDate { get; set; }
|
||||
public DateTime? PublicationDate { get; set; }
|
||||
public int CurrentWordCount { get; set; }
|
||||
public int ChapterCount { get; set; }
|
||||
public int SceneCount { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public DateTime UpdatedDate { get; set; }
|
||||
public bool IsArchived { get; set; }
|
||||
public DateTime? ArchivedDate { get; set; }
|
||||
public string? ArchivedReason { get; set; }
|
||||
public decimal? ProgressPercentage => TargetWordCount is > 0
|
||||
? Math.Min(100m, Math.Round(CurrentWordCount * 100m / TargetWordCount.Value, 1))
|
||||
: null;
|
||||
}
|
||||
|
||||
public static class BookStatuses
|
||||
{
|
||||
public const string Planning = "Planning";
|
||||
public const string Research = "Research";
|
||||
public const string Outlining = "Outlining";
|
||||
public const string Drafting = "Drafting";
|
||||
public const string Revising = "Revising";
|
||||
public const string Editing = "Editing";
|
||||
public const string Proofreading = "Proofreading";
|
||||
public const string ReadyForPublication = "Ready for Publication";
|
||||
public const string Published = "Published";
|
||||
public const string Paused = "Paused";
|
||||
public const string Archived = "Archived";
|
||||
|
||||
public static readonly IReadOnlyList<string> All =
|
||||
[
|
||||
Planning,
|
||||
Research,
|
||||
Outlining,
|
||||
Drafting,
|
||||
Revising,
|
||||
Editing,
|
||||
Proofreading,
|
||||
ReadyForPublication,
|
||||
Published,
|
||||
Paused,
|
||||
Archived
|
||||
];
|
||||
|
||||
public static bool IsValid(string? status) => All.Contains(status ?? string.Empty, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public sealed class Chapter
|
||||
|
||||
@ -83,6 +83,7 @@ public interface IBookService
|
||||
Task<BookDetailViewModel?> GetDetailAsync(int bookId);
|
||||
Task<BookEditViewModel?> GetCreateAsync(int projectId);
|
||||
Task<BookEditViewModel?> GetEditAsync(int bookId);
|
||||
Task PopulateEditContextAsync(BookEditViewModel model);
|
||||
Task<int> SaveAsync(BookEditViewModel model);
|
||||
Task ArchiveAsync(int bookId);
|
||||
}
|
||||
@ -799,7 +800,9 @@ public sealed class BookService(
|
||||
{
|
||||
ProjectID = projectId,
|
||||
Project = project,
|
||||
BookNumber = existing.Count + 1
|
||||
BookNumber = existing.Count + 1,
|
||||
Status = BookStatuses.Planning,
|
||||
StatusOptions = BuildStatusOptions(BookStatuses.Planning)
|
||||
};
|
||||
}
|
||||
|
||||
@ -816,12 +819,27 @@ public sealed class BookService(
|
||||
BookID = book.BookID,
|
||||
ProjectID = book.ProjectID,
|
||||
BookTitle = book.BookTitle,
|
||||
Subtitle = book.Subtitle,
|
||||
Tagline = book.Tagline,
|
||||
ShortDescription = book.ShortDescription,
|
||||
Status = book.Status,
|
||||
BookNumber = book.BookNumber,
|
||||
Description = book.Description,
|
||||
Project = await projects.GetAsync(book.ProjectID)
|
||||
TargetWordCount = book.TargetWordCount,
|
||||
WritingStartDate = book.WritingStartDate,
|
||||
TargetCompletionDate = book.TargetCompletionDate,
|
||||
PublicationDate = book.PublicationDate,
|
||||
Project = await projects.GetAsync(book.ProjectID),
|
||||
StatusOptions = BuildStatusOptions(book.Status)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task PopulateEditContextAsync(BookEditViewModel model)
|
||||
{
|
||||
model.Project = await projects.GetAsync(model.ProjectID);
|
||||
model.StatusOptions = BuildStatusOptions(model.Status);
|
||||
}
|
||||
|
||||
public async Task<int> SaveAsync(BookEditViewModel model)
|
||||
{
|
||||
if (model.BookID == 0)
|
||||
@ -829,19 +847,37 @@ public sealed class BookService(
|
||||
await SubscriptionLimitGuards.EnsureAllowedAsync(subscriptions.CanCreateBookAsync, currentUser.UserId);
|
||||
}
|
||||
|
||||
if (!BookStatuses.IsValid(model.Status))
|
||||
{
|
||||
model.Status = BookStatuses.Planning;
|
||||
}
|
||||
|
||||
var isNew = model.BookID == 0;
|
||||
var bookId = await books.SaveAsync(new Book
|
||||
{
|
||||
BookID = model.BookID,
|
||||
ProjectID = model.ProjectID,
|
||||
BookTitle = model.BookTitle,
|
||||
Subtitle = model.Subtitle,
|
||||
Tagline = model.Tagline,
|
||||
ShortDescription = model.ShortDescription,
|
||||
Status = model.Status,
|
||||
BookNumber = model.BookNumber,
|
||||
Description = model.Description
|
||||
Description = model.Description,
|
||||
TargetWordCount = model.TargetWordCount,
|
||||
WritingStartDate = model.WritingStartDate,
|
||||
TargetCompletionDate = model.TargetCompletionDate,
|
||||
PublicationDate = model.PublicationDate
|
||||
});
|
||||
await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Book", bookId, model.BookTitle);
|
||||
return bookId;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SelectListItem> BuildStatusOptions(string? selectedStatus)
|
||||
=> BookStatuses.All
|
||||
.Select(status => new SelectListItem(status, status, string.Equals(status, selectedStatus, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
|
||||
public async Task ArchiveAsync(int bookId)
|
||||
{
|
||||
var book = await books.GetAsync(bookId);
|
||||
|
||||
227
PlotLine/Sql/066_Phase13A_BookMetadataBackend.sql
Normal file
227
PlotLine/Sql/066_Phase13A_BookMetadataBackend.sql
Normal file
@ -0,0 +1,227 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Books', N'Subtitle') IS NULL
|
||||
ALTER TABLE dbo.Books ADD Subtitle nvarchar(200) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Books', N'Tagline') IS NULL
|
||||
ALTER TABLE dbo.Books ADD Tagline nvarchar(300) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Books', N'ShortDescription') IS NULL
|
||||
ALTER TABLE dbo.Books ADD ShortDescription nvarchar(max) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Books', N'Status') IS NULL
|
||||
ALTER TABLE dbo.Books ADD Status nvarchar(50) NOT NULL CONSTRAINT DF_Books_Status DEFAULT N'Planning';
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Books', N'TargetWordCount') IS NULL
|
||||
ALTER TABLE dbo.Books ADD TargetWordCount int NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Books', N'WritingStartDate') IS NULL
|
||||
ALTER TABLE dbo.Books ADD WritingStartDate date NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Books', N'TargetCompletionDate') IS NULL
|
||||
ALTER TABLE dbo.Books ADD TargetCompletionDate date NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Books', N'PublicationDate') IS NULL
|
||||
ALTER TABLE dbo.Books ADD PublicationDate date NULL;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.check_constraints WHERE name = N'CK_Books_TargetWordCount' AND parent_object_id = OBJECT_ID(N'dbo.Books'))
|
||||
ALTER TABLE dbo.Books ADD CONSTRAINT CK_Books_TargetWordCount CHECK (TargetWordCount IS NULL OR TargetWordCount > 0);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Book_ListByProject
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT
|
||||
b.BookID,
|
||||
b.ProjectID,
|
||||
b.BookTitle,
|
||||
b.Subtitle,
|
||||
b.Tagline,
|
||||
b.ShortDescription,
|
||||
b.Status,
|
||||
b.BookNumber,
|
||||
b.Description,
|
||||
b.TargetWordCount,
|
||||
b.WritingStartDate,
|
||||
b.TargetCompletionDate,
|
||||
b.PublicationDate,
|
||||
ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount,
|
||||
ISNULL(counts.ChapterCount, 0) AS ChapterCount,
|
||||
ISNULL(counts.SceneCount, 0) AS SceneCount,
|
||||
b.SortOrder,
|
||||
b.CreatedDate,
|
||||
b.UpdatedDate,
|
||||
b.IsArchived,
|
||||
b.ArchivedDate,
|
||||
b.ArchivedReason
|
||||
FROM dbo.Books b
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT
|
||||
COUNT(DISTINCT c.ChapterID) AS ChapterCount,
|
||||
COUNT(s.SceneID) AS SceneCount
|
||||
FROM dbo.Chapters c
|
||||
LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) counts
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount
|
||||
FROM dbo.Chapters c
|
||||
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) wordCounts
|
||||
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0
|
||||
ORDER BY b.SortOrder, b.BookNumber, b.BookTitle;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Book_Get
|
||||
@BookID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT
|
||||
b.BookID,
|
||||
b.ProjectID,
|
||||
b.BookTitle,
|
||||
b.Subtitle,
|
||||
b.Tagline,
|
||||
b.ShortDescription,
|
||||
b.Status,
|
||||
b.BookNumber,
|
||||
b.Description,
|
||||
b.TargetWordCount,
|
||||
b.WritingStartDate,
|
||||
b.TargetCompletionDate,
|
||||
b.PublicationDate,
|
||||
ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount,
|
||||
ISNULL(counts.ChapterCount, 0) AS ChapterCount,
|
||||
ISNULL(counts.SceneCount, 0) AS SceneCount,
|
||||
b.SortOrder,
|
||||
b.CreatedDate,
|
||||
b.UpdatedDate,
|
||||
b.IsArchived,
|
||||
b.ArchivedDate,
|
||||
b.ArchivedReason
|
||||
FROM dbo.Books b
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT
|
||||
COUNT(DISTINCT c.ChapterID) AS ChapterCount,
|
||||
COUNT(s.SceneID) AS SceneCount
|
||||
FROM dbo.Chapters c
|
||||
LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) counts
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount
|
||||
FROM dbo.Chapters c
|
||||
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) wordCounts
|
||||
WHERE b.BookID = @BookID AND b.IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Book_Save
|
||||
@BookID int = NULL,
|
||||
@ProjectID int,
|
||||
@BookTitle nvarchar(200),
|
||||
@Subtitle nvarchar(200) = NULL,
|
||||
@Tagline nvarchar(300) = NULL,
|
||||
@ShortDescription nvarchar(max) = NULL,
|
||||
@Status nvarchar(50) = N'Planning',
|
||||
@BookNumber int,
|
||||
@Description nvarchar(max) = NULL,
|
||||
@TargetWordCount int = NULL,
|
||||
@WritingStartDate date = NULL,
|
||||
@TargetCompletionDate date = NULL,
|
||||
@PublicationDate date = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @TargetWordCount IS NOT NULL AND @TargetWordCount <= 0
|
||||
THROW 51000, 'Target word count must be greater than zero.', 1;
|
||||
|
||||
IF NULLIF(LTRIM(RTRIM(@Status)), N'') IS NULL
|
||||
SET @Status = N'Planning';
|
||||
|
||||
IF @BookID IS NULL OR @BookID = 0
|
||||
BEGIN
|
||||
DECLARE @NextBookOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Books WHERE ProjectID = @ProjectID), 0) + 10;
|
||||
|
||||
INSERT dbo.Books
|
||||
(
|
||||
ProjectID,
|
||||
BookTitle,
|
||||
Subtitle,
|
||||
Tagline,
|
||||
ShortDescription,
|
||||
Status,
|
||||
BookNumber,
|
||||
Description,
|
||||
TargetWordCount,
|
||||
WritingStartDate,
|
||||
TargetCompletionDate,
|
||||
PublicationDate,
|
||||
SortOrder
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@ProjectID,
|
||||
@BookTitle,
|
||||
@Subtitle,
|
||||
@Tagline,
|
||||
@ShortDescription,
|
||||
@Status,
|
||||
@BookNumber,
|
||||
@Description,
|
||||
@TargetWordCount,
|
||||
@WritingStartDate,
|
||||
@TargetCompletionDate,
|
||||
@PublicationDate,
|
||||
@NextBookOrder
|
||||
);
|
||||
|
||||
SELECT CAST(SCOPE_IDENTITY() AS int) AS BookID;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
UPDATE dbo.Books
|
||||
SET BookTitle = @BookTitle,
|
||||
Subtitle = @Subtitle,
|
||||
Tagline = @Tagline,
|
||||
ShortDescription = @ShortDescription,
|
||||
Status = @Status,
|
||||
BookNumber = @BookNumber,
|
||||
Description = @Description,
|
||||
TargetWordCount = @TargetWordCount,
|
||||
WritingStartDate = @WritingStartDate,
|
||||
TargetCompletionDate = @TargetCompletionDate,
|
||||
PublicationDate = @PublicationDate,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE BookID = @BookID;
|
||||
|
||||
SELECT @BookID AS BookID;
|
||||
END;
|
||||
GO
|
||||
@ -65,11 +65,40 @@ public sealed class BookEditViewModel
|
||||
[Display(Name = "Book title")]
|
||||
public string BookTitle { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(200)]
|
||||
public string? Subtitle { get; set; }
|
||||
|
||||
[StringLength(300)]
|
||||
public string? Tagline { get; set; }
|
||||
|
||||
[Display(Name = "Short description")]
|
||||
public string? ShortDescription { get; set; }
|
||||
|
||||
[Required, StringLength(50)]
|
||||
public string Status { get; set; } = BookStatuses.Planning;
|
||||
|
||||
[Display(Name = "Book number")]
|
||||
public int BookNumber { get; set; } = 1;
|
||||
|
||||
[Display(Name = "Target word count")]
|
||||
[Range(1, int.MaxValue, ErrorMessage = "Target word count must be greater than zero.")]
|
||||
public int? TargetWordCount { get; set; }
|
||||
|
||||
[DataType(DataType.Date)]
|
||||
[Display(Name = "Writing start date")]
|
||||
public DateTime? WritingStartDate { get; set; }
|
||||
|
||||
[DataType(DataType.Date)]
|
||||
[Display(Name = "Target completion date")]
|
||||
public DateTime? TargetCompletionDate { get; set; }
|
||||
|
||||
[DataType(DataType.Date)]
|
||||
[Display(Name = "Publication date")]
|
||||
public DateTime? PublicationDate { get; set; }
|
||||
|
||||
public string? Description { get; set; }
|
||||
public Project? Project { get; set; }
|
||||
public IReadOnlyList<SelectListItem> StatusOptions { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class BookDetailViewModel
|
||||
|
||||
@ -16,6 +16,14 @@
|
||||
<div>
|
||||
<p class="eyebrow"><a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a></p>
|
||||
<h1>Book @Model.Book.BookNumber: @Model.Book.BookTitle <help-icon key="books.overview" /></h1>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Book.Subtitle))
|
||||
{
|
||||
<p class="lead-text">@Model.Book.Subtitle</p>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Book.Tagline))
|
||||
{
|
||||
<p class="muted">@Model.Book.Tagline</p>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Book.Description))
|
||||
{
|
||||
<p class="lead-text">@Model.Book.Description</p>
|
||||
@ -41,6 +49,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="list-section">
|
||||
<h2>Book metadata</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<span class="status-pill">@Model.Book.Status</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>@Model.Book.CurrentWordCount.ToString("N0")</strong>
|
||||
<span class="muted">current words</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>@(Model.Book.TargetWordCount?.ToString("N0") ?? "Not set")</strong>
|
||||
<span class="muted">target words</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>@(Model.Book.ProgressPercentage.HasValue ? $"{Model.Book.ProgressPercentage:0.#}%" : "Not set")</strong>
|
||||
<span class="muted">progress</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>@Model.Book.ChapterCount.ToString("N0")</strong>
|
||||
<span class="muted">chapters</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>@Model.Book.SceneCount.ToString("N0")</strong>
|
||||
<span class="muted">scenes</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>@(Model.Book.WritingStartDate?.ToString("dd MMM yyyy") ?? "Not set")</strong>
|
||||
<span class="muted">writing start</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>@(Model.Book.TargetCompletionDate?.ToString("dd MMM yyyy") ?? "Not set")</strong>
|
||||
<span class="muted">target completion</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<strong>@(Model.Book.PublicationDate?.ToString("dd MMM yyyy") ?? "Not set")</strong>
|
||||
<span class="muted">publication</span>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Book.ShortDescription))
|
||||
{
|
||||
<div class="col-12">
|
||||
<p>@Model.Book.ShortDescription</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="list-section">
|
||||
<h2>Chapters <help-icon key="books.chapters" /></h2>
|
||||
@if (!Model.Chapters.Any())
|
||||
|
||||
@ -25,6 +25,45 @@
|
||||
<input asp-for="BookTitle" class="form-control" />
|
||||
<span asp-validation-for="BookTitle" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Subtitle" class="form-label"></label>
|
||||
<input asp-for="Subtitle" class="form-control" />
|
||||
<span asp-validation-for="Subtitle" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Tagline" class="form-label"></label>
|
||||
<input asp-for="Tagline" class="form-control" />
|
||||
<span asp-validation-for="Tagline" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="Status" class="form-label"></label>
|
||||
<select asp-for="Status" asp-items="Model.StatusOptions" class="form-select"></select>
|
||||
<span asp-validation-for="Status" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="TargetWordCount" class="form-label"></label>
|
||||
<input asp-for="TargetWordCount" class="form-control" min="1" />
|
||||
<span asp-validation-for="TargetWordCount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="WritingStartDate" class="form-label"></label>
|
||||
<input asp-for="WritingStartDate" class="form-control" />
|
||||
<span asp-validation-for="WritingStartDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="TargetCompletionDate" class="form-label"></label>
|
||||
<input asp-for="TargetCompletionDate" class="form-control" />
|
||||
<span asp-validation-for="TargetCompletionDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="PublicationDate" class="form-label"></label>
|
||||
<input asp-for="PublicationDate" class="form-control" />
|
||||
<span asp-validation-for="PublicationDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label asp-for="ShortDescription" class="form-label"></label>
|
||||
<textarea asp-for="ShortDescription" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label asp-for="Description" class="form-label"></label>
|
||||
<textarea asp-for="Description" class="form-control" rows="4"></textarea>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user