259 lines
13 KiB
Plaintext

@model StoryCalendarViewModel
@using System.Globalization
@{
ViewData["Title"] = $"Calendar - {Model.Project.ProjectName}";
ViewData["ProjectSection"] = "Calendar";
var selectedMonthValue = Model.SelectedMonth?.ToString("yyyy-MM", CultureInfo.InvariantCulture);
var selectedDateValue = Model.SelectedDate?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
var showBookLabels = !Model.SelectedBookID.HasValue;
var weekdays = new[] { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
<span>Calendar</span>
</nav>
<partial name="_ProjectSectionNav" model="Model.Project" />
<div class="page-heading story-calendar-heading">
<div>
<p class="eyebrow">Story time</p>
<h1>Calendar</h1>
<p class="lead-text">@Model.StoryRangeLabel</p>
</div>
<div class="story-calendar-stats" aria-label="Calendar summary">
<span><strong>@Model.SceneCount</strong> dated scenes</span>
<span><strong>@Model.ActiveStoryDayCount</strong> story days</span>
<span><strong>@Model.UnplacedScenes.Count</strong> unplaced</span>
</div>
</div>
<section class="card mb-3 story-calendar-filter">
<div class="card-body">
<form asp-action="ApplyFilters" method="post" class="row g-2 align-items-end">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<div class="col-md-5 col-lg-4">
<label class="form-label" for="bookId">Book</label>
<select class="form-select" id="bookId" name="bookId" asp-items="Model.BookOptions"></select>
</div>
<div class="col-md-4 col-lg-3">
<label class="form-label" for="month">Month</label>
<select class="form-select" id="month" name="month" disabled="@(!Model.MonthOptions.Any())">
@if (!Model.MonthOptions.Any())
{
<option value="">No dated scenes</option>
}
else
{
@foreach (var option in Model.MonthOptions)
{
<option value="@option.Value" selected="@option.Selected">@option.Text</option>
}
}
</select>
</div>
<div class="col-md-3 col-lg-2">
<button class="btn btn-primary w-100" type="submit">Apply</button>
</div>
</form>
</div>
</section>
@if (!Model.HasDatedScenes)
{
<section class="card story-calendar-empty">
<div class="card-body">
<h2>No dated scenes yet</h2>
<p class="muted mb-0">Scenes will appear here once canonical scene chronology has a resolved start date.</p>
</div>
</section>
}
else
{
<section class="story-calendar-shell">
<div class="story-calendar-toolbar">
<div class="button-row">
<a class="btn btn-outline-secondary btn-sm"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-month="@Model.PreviousMonth?.ToString("yyyy-MM", CultureInfo.InvariantCulture)">Previous month</a>
<a class="btn btn-outline-secondary btn-sm"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-month="@Model.NextMonth?.ToString("yyyy-MM", CultureInfo.InvariantCulture)">Next month</a>
@if (Model.FirstStoryMonth.HasValue)
{
<a class="btn btn-outline-secondary btn-sm"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-month="@Model.FirstStoryMonth.Value.ToString("yyyy-MM", CultureInfo.InvariantCulture)">First dated scene</a>
}
</div>
<div class="story-calendar-story-months">
@if (Model.PreviousStoryMonth.HasValue)
{
<a asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-month="@Model.PreviousStoryMonth.Value.ToString("yyyy-MM", CultureInfo.InvariantCulture)">Previous story month</a>
}
@if (Model.NextStoryMonth.HasValue)
{
<a asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-month="@Model.NextStoryMonth.Value.ToString("yyyy-MM", CultureInfo.InvariantCulture)">Next story month</a>
}
</div>
</div>
<div class="story-calendar-layout">
<div class="story-calendar-grid-wrap">
<div class="story-calendar-month" aria-label="@Model.SelectedMonthLabel">
<div class="story-calendar-month-title">@Model.SelectedMonthLabel</div>
<div class="story-calendar-weekdays">
@foreach (var weekday in weekdays)
{
<div>@weekday</div>
}
</div>
<div class="story-calendar-grid">
@foreach (var week in Model.Weeks)
{
@foreach (var day in week.Days)
{
var visibleScenes = day.Scenes.Take(3).ToList();
var hiddenCount = Math.Max(0, day.Scenes.Count - visibleScenes.Count);
<a class="story-calendar-day @(day.IsInSelectedMonth ? "" : "is-overflow") @(day.IsSelected ? "is-selected" : "")"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-month="@selectedMonthValue"
asp-route-date="@day.QueryValue"
aria-label="@day.DayLabel">
<span class="story-calendar-day-number">@day.DayNumber</span>
<span class="story-calendar-day-scenes">
@foreach (var scene in visibleScenes)
{
<span class="story-calendar-scene-chip">
@if (showBookLabels)
{
<span class="story-calendar-book">@scene.BookLabel</span>
}
<span>@scene.SceneLabel</span>
<strong>@scene.Title</strong>
</span>
}
@if (hiddenCount > 0)
{
<span class="story-calendar-more">+ @hiddenCount more</span>
}
</span>
</a>
}
}
</div>
</div>
</div>
<aside class="story-calendar-detail" aria-label="Selected day">
@if (Model.SelectedDay is null)
{
<h2>Select a story day</h2>
<p class="muted">Choose a date in the grid to inspect its scenes in narrative order.</p>
}
else
{
<h2>@Model.SelectedDay.DayLabel</h2>
@if (!Model.SelectedDay.Scenes.Any())
{
<p class="muted">No scenes are placed on this story date.</p>
}
else
{
<div class="story-calendar-detail-list">
@foreach (var scene in Model.SelectedDay.Scenes)
{
<article class="story-calendar-detail-scene">
<div class="story-calendar-detail-meta">
@if (showBookLabels)
{
<span>@scene.BookTitle</span>
}
<span>Chapter @scene.ChapterNumber:g</span>
<span>Scene @scene.SceneNumber:g</span>
</div>
<h3>@scene.Title</h3>
<dl>
@if (!string.IsNullOrWhiteSpace(scene.Pov))
{
<dt>POV</dt>
<dd>@scene.Pov</dd>
}
<dt>Time</dt>
<dd>@scene.TimeLabel</dd>
</dl>
@if (!string.IsNullOrWhiteSpace(scene.Summary))
{
<p>@scene.Summary</p>
}
<div class="button-row">
<a class="btn btn-outline-primary btn-sm" asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">Open scene</a>
<a class="btn btn-outline-secondary btn-sm"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@scene.BookID"
asp-route-selectedSceneId="@scene.SceneID">Open in Timeline</a>
</div>
</article>
}
</div>
}
}
</aside>
</div>
</section>
}
<section class="card story-calendar-unplaced">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Unplaced scenes</strong>
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#story-calendar-unplaced" aria-expanded="false" aria-controls="story-calendar-unplaced">
@Model.UnplacedScenes.Count scenes
</button>
</div>
<div class="collapse" id="story-calendar-unplaced">
<div class="card-body">
@if (!Model.UnplacedScenes.Any())
{
<p class="muted mb-0">All scenes in this scope have resolved story dates.</p>
}
else
{
<div class="story-calendar-unplaced-list">
@foreach (var scene in Model.UnplacedScenes)
{
<article>
<div>
<strong>@scene.Title</strong>
<span>@scene.BookTitle · Chapter @scene.ChapterNumber:g · Scene @scene.SceneNumber:g</span>
@if (!string.IsNullOrWhiteSpace(scene.RelativeTimeText))
{
<small>@scene.RelativeTimeText</small>
}
</div>
<a class="btn btn-outline-primary btn-sm" asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">Open scene</a>
</article>
}
</div>
}
</div>
</div>
</section>