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; }