From a9f614bc524ba02d499dd084f95e488161c7f711 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 14:59:36 +0000 Subject: [PATCH] Show planned delete date prominently on box gallery page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any visitor now sees a colour-coded expiry banner whenever a box has a delete date set, not just when it is within 7 days. The banner shows: - The exact date ("4 Jul 2026") plus a human relative label ("in 3 months") - A ⚠️ icon and urgent call-to-action when deletion is imminent Colour urgency tiers (light + dark theme): - critical (red) ≤ 7 days - warning (orange) ≤ 30 days - notice (yellow) ≤ 90 days - info (blue) > 90 days Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_019sA2fUMbEuMe8QTgfJanif --- src/Shoebox.Web/Pages/Pool/Gallery.cshtml | 14 +++++++-- src/Shoebox.Web/Pages/Pool/Gallery.cshtml.cs | 32 +++++++++++++++++++- src/Shoebox.Web/wwwroot/css/site.css | 17 +++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/Shoebox.Web/Pages/Pool/Gallery.cshtml b/src/Shoebox.Web/Pages/Pool/Gallery.cshtml index 0c687c7..2c029b8 100644 --- a/src/Shoebox.Web/Pages/Pool/Gallery.cshtml +++ b/src/Shoebox.Web/Pages/Pool/Gallery.cshtml @@ -28,9 +28,19 @@ · from @Model.Uploaders.Count @(Model.Uploaders.Count == 1 ? "person" : "people") }

- @if (Model.ExpiresSoon) + @if (Model.Pool.ExpiresAt.HasValue) { - + } diff --git a/src/Shoebox.Web/Pages/Pool/Gallery.cshtml.cs b/src/Shoebox.Web/Pages/Pool/Gallery.cshtml.cs index ac91804..585f057 100644 --- a/src/Shoebox.Web/Pages/Pool/Gallery.cshtml.cs +++ b/src/Shoebox.Web/Pages/Pool/Gallery.cshtml.cs @@ -25,6 +25,8 @@ public class GalleryModel( public string ShareUrl { get; set; } = ""; public bool IsAdmin { get; set; } public bool ExpiresSoon { get; set; } + public string ExpiryLabel { get; set; } = ""; + public string ExpiryUrgency { get; set; } = ""; // Whether the box holds any photos the current visitor did not upload, so the // "everyone else's" download only offers itself when it would actually return files. @@ -75,7 +77,35 @@ public async Task OnGetAsync(string code) UploaderName = identity.GetName(HttpContext) ?? ""; ShareUrl = links.PoolUrl(HttpContext, pool); IsAdmin = access.IsAdmin(HttpContext, pool.Id); - ExpiresSoon = pool.ExpiresAt is { } exp && exp < DateTime.UtcNow.AddDays(7); + if (pool.ExpiresAt is { } exp) + { + ExpiresSoon = exp < DateTime.UtcNow.AddDays(7); + ExpiryLabel = BuildExpiryLabel(exp); + ExpiryUrgency = BuildExpiryUrgency(exp); + } return Page(); } + + private static string BuildExpiryLabel(DateTime expiresAt) + { + var diff = expiresAt - DateTime.UtcNow; + if (diff.TotalHours < 24) return "today"; + if (diff.TotalDays < 2) return "tomorrow"; + if (diff.TotalDays < 7) return $"in {(int)diff.TotalDays} day{((int)diff.TotalDays == 1 ? "" : "s")}"; + if (diff.TotalDays < 14) return "in 1 week"; + if (diff.TotalDays < 30) return $"in {(int)(diff.TotalDays / 7)} weeks"; + if (diff.TotalDays < 60) return "in 1 month"; + if (diff.TotalDays < 365) return $"in {(int)(diff.TotalDays / 30)} months"; + if (diff.TotalDays < 730) return "in 1 year"; + return $"in {(int)(diff.TotalDays / 365)} years"; + } + + private static string BuildExpiryUrgency(DateTime expiresAt) + { + var days = (expiresAt - DateTime.UtcNow).TotalDays; + if (days <= 7) return "critical"; + if (days <= 30) return "warning"; + if (days <= 90) return "notice"; + return "info"; + } } diff --git a/src/Shoebox.Web/wwwroot/css/site.css b/src/Shoebox.Web/wwwroot/css/site.css index 6a10ab8..84b42b8 100644 --- a/src/Shoebox.Web/wwwroot/css/site.css +++ b/src/Shoebox.Web/wwwroot/css/site.css @@ -439,6 +439,23 @@ input[type="checkbox"] { accent-color: var(--ink); } :root[data-theme="dark"] .banner.warn { background: #2a2410; border-color: #6b5a1e; color: #ecd9a2; } :root[data-theme="dark"] .banner.success { background: #142319; border-color: #2f5b3d; color: #a9d9b6; } +/* Expiry banner urgency levels */ +.expiry-banner { display: flex; align-items: baseline; gap: 0.5rem; } +.expiry-icon { font-size: 1.1em; flex-shrink: 0; } +.expiry-text { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem; line-height: 1.4; } +.expiry-relative { font-size: 0.9em; opacity: 0.85; } +.expiry-action { font-size: 0.9em; } + +.banner.expiry-critical { background: #fff0f0; border-color: #e03a3a; color: #7a0000; font-weight: 600; } +.banner.expiry-warning { background: #fff7ed; border-color: #d97706; color: #7c3500; } +.banner.expiry-notice { background: #fdf6e3; border-color: #d9b23a; color: #5a4a12; } +.banner.expiry-info { background: #f0f6ff; border-color: #6b9fd4; color: #1a3a5c; } + +:root[data-theme="dark"] .banner.expiry-critical { background: #2d0a0a; border-color: #c03030; color: #ffb3b3; } +:root[data-theme="dark"] .banner.expiry-warning { background: #2a1800; border-color: #c07a00; color: #ffd080; } +:root[data-theme="dark"] .banner.expiry-notice { background: #2a2410; border-color: #6b5a1e; color: #ecd9a2; } +:root[data-theme="dark"] .banner.expiry-info { background: #0d1e30; border-color: #3a6a9f; color: #a8c8e8; } + .admin-link { margin-top: 0.55rem; font-family: var(--mono); font-size: 0.8rem; } .banner-cta { margin-top: 0.85rem; }