Skip to content

Commit cd9d300

Browse files
Copilotrvanmaanen
andauthored
Fix newsletter pages: fix mobile banner title, fix Home/Newsletter subnav behavior, and preserve stats on refresh (#449)
* Initial plan * fix: remove newsletter homepage button, fix mobile banner title, fix newsletter button color * feat: add Home nav button and active-state highlighting for Home/Newsletter in homepage SubNav * fix: guard test FindIndex results and clarify CSS cascade comment * fix: match homepage subnav button gap to section pages (spacing-1) * fix: keep newsletter stats on refresh and reorder homepage toolbar buttons * refactor: clarify subnav stats fetch gating and ordering tests * fix: adjust mobile homepage subnav stat chip visibility * test: cover mobile new label markup hook in SubNav * fix: hide total stat chip on mobile subnav --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Reinier <reinier.vanmaanen@xebia.com>
1 parent c6ec3bc commit cd9d300

7 files changed

Lines changed: 368 additions & 57 deletions

File tree

src/TechHub.Web/Components/Header.razor

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
With custom banner (home, about, not-found, error pages):
1414
<Header CustomBannerTitle="Tech Hub" CustomBannerDescription="..." RssFeedUrl="/all/feed.xml" ShowNewsletter="true" />
1515
<Header CustomBannerTitle="About Us" CustomBannerDescription="..." HideStats="true" ShowDiscord="true" />
16-
<Header CustomBannerTitle="Newsletter" CustomBannerDescription="..." ShowHomepageButton="true" />
16+
<Header CustomBannerTitle="Newsletter" CustomBannerDescription="..." />
1717
1818
Without section/banner (fallback):
1919
<Header />
@@ -37,20 +37,20 @@
3737
}
3838
else if (!string.IsNullOrEmpty(CustomBannerTitle))
3939
{
40-
<SectionBanner Title="@CustomBannerTitle" Description="@CustomBannerDescription" RssFeedUrl="@RssFeedUrl" />
40+
<SectionBanner Title="@CustomBannerTitle" Description="@CustomBannerDescription" RssFeedUrl="@RssFeedUrl" IsHomepage="@IsHomepage" />
4141
@if (!HideSubNav)
4242
{
43-
<SubNav Sections="@SectionCache.Sections" RssFeedUrl="@RssFeedUrl" ShowNewsletter="@ShowNewsletter" HideStats="@HideStats" ShowDiscord="@ShowDiscord" ShowHomepageButton="@ShowHomepageButton" />
43+
<SubNav Sections="@SectionCache.Sections" RssFeedUrl="@RssFeedUrl" ShowNewsletter="@ShowNewsletter" HideStats="@HideStats" ShowDiscord="@ShowDiscord" ShowHomeButton="@ShowHomeButton" />
4444
}
4545
}
4646
else if (ChildContent != null)
4747
{
4848
@ChildContent
49-
<SubNav Sections="@SectionCache.Sections" RssFeedUrl="@RssFeedUrl" ShowNewsletter="@ShowNewsletter" HideStats="@HideStats" ShowDiscord="@ShowDiscord" ShowHomepageButton="@ShowHomepageButton" />
49+
<SubNav Sections="@SectionCache.Sections" RssFeedUrl="@RssFeedUrl" ShowNewsletter="@ShowNewsletter" HideStats="@HideStats" ShowDiscord="@ShowDiscord" ShowHomeButton="@ShowHomeButton" />
5050
}
5151
else
5252
{
53-
<SubNav Sections="@SectionCache.Sections" RssFeedUrl="@RssFeedUrl" ShowNewsletter="@ShowNewsletter" HideStats="@HideStats" ShowDiscord="@ShowDiscord" ShowHomepageButton="@ShowHomepageButton" />
53+
<SubNav Sections="@SectionCache.Sections" RssFeedUrl="@RssFeedUrl" ShowNewsletter="@ShowNewsletter" HideStats="@HideStats" ShowDiscord="@ShowDiscord" ShowHomeButton="@ShowHomeButton" />
5454
}
5555
</header>
5656

@@ -83,10 +83,16 @@
8383
public bool ShowDiscord { get; set; }
8484

8585
/// <summary>
86-
/// When true, renders a Homepage button in the SubNav. Used on the Newsletter page.
86+
/// When true, renders a "Home" button in the SubNav that is highlighted when on the homepage.
8787
/// </summary>
8888
[Parameter]
89-
public bool ShowHomepageButton { get; set; }
89+
public bool ShowHomeButton { get; set; }
90+
91+
/// <summary>
92+
/// When true, renders the SectionBanner with homepage-specific styling and "Home" as the mobile title.
93+
/// </summary>
94+
[Parameter]
95+
public bool IsHomepage { get; set; }
9096

9197
/// <summary>
9298
/// When true, hides the SubNav completely. Used on pages where navigation is not needed (e.g. Newsletter).

src/TechHub.Web/Components/Layout/MainLayout.razor

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
CustomBannerDescription="@_customBannerDescription"
1919
HideStats="@_hideStats"
2020
ShowDiscord="@_showDiscord"
21-
ShowHomepageButton="@_showHomepageButton"
21+
IsHomepage="@_isHomepage"
22+
ShowHomeButton="@_showHomeButton"
2223
HideSubNav="@_hideSubNav" />
2324

2425
@if (_isInvalid)
@@ -65,7 +66,8 @@ else
6566
private bool _showNewsletter;
6667
private bool _hideStats;
6768
private bool _showDiscord;
68-
private bool _showHomepageButton;
69+
private bool _isHomepage;
70+
private bool _showHomeButton;
6971
private bool _hideSubNav;
7072
private bool _isInvalid;
7173
private TechHub.Core.Models.Section? _currentSection;
@@ -110,7 +112,8 @@ else
110112
_showNewsletter = false;
111113
_hideStats = false;
112114
_showDiscord = false;
113-
_showHomepageButton = false;
115+
_isHomepage = false;
116+
_showHomeButton = false;
114117
_hideSubNav = false;
115118

116119
// Try to match first URL segment to a known section
@@ -166,6 +169,8 @@ else
166169
_customBannerDescription = "Your source for Microsoft technology news, tutorials, and community content";
167170
_currentRssFeedUrl = "/all/feed.xml";
168171
_showNewsletter = true;
172+
_showHomeButton = true;
173+
_isHomepage = true;
169174
break;
170175
case "about":
171176
_customBannerTitle = "About Us";
@@ -176,7 +181,8 @@ else
176181
case "newsletter":
177182
_customBannerTitle = "Newsletter";
178183
_customBannerDescription = "Subscribe to weekly roundups and daily overviews for your favourite sections";
179-
_showHomepageButton = true;
184+
_showNewsletter = true;
185+
_showHomeButton = true;
180186
break;
181187
case "not-found":
182188
_customBannerTitle = "Not Found";

src/TechHub.Web/Components/SectionBanner.razor

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,24 @@
5050
[Parameter]
5151
public Section? Section { get; set; }
5252

53+
/// <summary>
54+
/// When true, renders the banner with homepage-specific styling (home-banner CSS class)
55+
/// and shows "Home" as the mobile title instead of the page title.
56+
/// </summary>
57+
[Parameter]
58+
public bool IsHomepage { get; set; }
59+
5360
// Computed properties - prefer Section data if available
5461
private string DisplayTitle => Section?.Title ?? Title ?? string.Empty;
5562
private string? DisplayDescription => Section?.Description ?? Description;
5663
private string BackgroundClass => Section != null
5764
? $"section-banner-bg-{Section.Name}"
5865
: BackgroundCssClass ?? "section-banner-bg-none";
5966

60-
private string CssClass => Section == null && string.IsNullOrEmpty(BackgroundCssClass) ? "home-banner" : "";
67+
private string CssClass => IsHomepage ? "home-banner" : "";
6168

6269
/// <summary>
6370
/// Mobile title: shows "Home" for homepage, otherwise same as DisplayTitle.
6471
/// </summary>
65-
private string MobileTitle => Section == null && string.IsNullOrEmpty(BackgroundCssClass) ? "Home" : DisplayTitle;
72+
private string MobileTitle => IsHomepage ? "Home" : DisplayTitle;
6673
}

src/TechHub.Web/Components/SubNav.razor

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
Section page: <SubNav Section="@sectionData" RssFeedUrl="/ai/feed.xml" ShowNewsletter="true" />
1010
Homepage: <SubNav Sections="@allSections" RssFeedUrl="/all/feed.xml" ShowNewsletter="true" />
1111
About page: <SubNav Sections="@allSections" HideStats="true" ShowDiscord="true" />
12-
Newsletter: <SubNav Sections="@allSections" ShowHomepageButton="true" />
12+
Newsletter: <SubNav Sections="@allSections" />
1313
============================================================================ *@
1414

1515
@using TechHub.Core.Models
1616
@using TechHub.Web.Services
1717
@inject NavigationManager Navigation
1818
@inject IJSRuntime JS
1919
@inject HomepageStatsState HomepageStats
20+
@inject ITechHubApiClient ApiClient
2021
@implements IDisposable
2122

2223
@if (Section != null)
@@ -86,43 +87,43 @@
8687
</div>
8788
</nav>
8889
}
89-
else if (Sections != null && Sections.Any())
90+
else if (Sections != null && HasHomepageContent)
9091
{
9192
@* Homepage SubNav: stat buttons + Newsletter; visible on desktop and mobile (no dropdown) *@
9293
<nav class="sub-nav sub-nav-homepage-visible" aria-label="Site navigation">
9394
<div class="sub-nav-wrapper">
95+
@if (ShowHomeButton)
96+
{
97+
<a href="/" class="btn-subnav @(IsHomepageActive() ? "active" : "")" aria-label="Homepage">
98+
Home
99+
</a>
100+
}
101+
@if (ShowNewsletter)
102+
{
103+
<a href="/newsletter/subscribe" class="btn-subnav btn-subnav-action @(IsActive("/newsletter") ? "active" : "")" aria-label="Subscribe to newsletter">
104+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" /></svg>
105+
Newsletter
106+
</a>
107+
}
94108
@if (!HideStats && HomepageStats.TotalCount.HasValue)
95109
{
96110
@if (HomepageStats.RecentCount is > 0)
97111
{
98112
<a href="/all?from=@HomepageStats.WeekAgoDate" class="btn-subnav btn-subnav-action btn-subnav-stat-new" aria-label="@HomepageStats.RecentCount new items this week">
99-
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M13.5 0.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"/></svg><span>@HomepageStats.RecentCount<span class="stat-chip-new-label"> new this week</span></span>
113+
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M13.5 0.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"/></svg><span>@HomepageStats.RecentCount<span class="stat-chip-new-label"> new<span class="stat-chip-new-label-suffix"> this week</span></span></span>
100114
</a>
101115
}
102116
<a href="/all" class="btn-subnav btn-subnav-action btn-subnav-stat-total" aria-label="@HomepageStats.TotalCount items total">
103117
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M11.99 18.54l-7.37-5.73L3 14.07l9 7 9-7-1.63-1.27-7.38 5.74zM12 16l7.36-5.73L21 9l-9-7-9 7 1.63 1.27L12 16z"/></svg><span>@HomepageStats.TotalCount.Value.ToString("N0", System.Globalization.CultureInfo.InvariantCulture)<span class="stat-chip-total-label"> items total</span></span>
104118
</a>
105119
}
106-
@if (ShowNewsletter)
107-
{
108-
<a href="/newsletter/subscribe" class="btn-subnav btn-subnav-action btn-subnav-newsletter" aria-label="Subscribe to newsletter">
109-
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" /></svg>
110-
Newsletter
111-
</a>
112-
}
113120
@if (ShowDiscord)
114121
{
115122
<a href="https://discord.gg/cURHV9TvFS" target="_blank" rel="noopener noreferrer" class="btn-subnav btn-subnav-action btn-subnav-discord" aria-label="Join our Discord server">
116123
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057.1 18.08.114 18.1.132 18.112a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
117124
Find us on GitHub Community Discord
118125
</a>
119126
}
120-
@if (ShowHomepageButton)
121-
{
122-
<a href="/" class="btn-subnav btn-subnav-action btn-subnav-newsletter">
123-
Homepage
124-
</a>
125-
}
126127
</div>
127128
</nav>
128129
}
@@ -144,6 +145,13 @@ else if (Sections != null && Sections.Any())
144145
[Parameter]
145146
public string? RssFeedUrl { get; set; }
146147

148+
/// <summary>
149+
/// When true, renders a "Home" button to the left of the stats in the homepage-style SubNav.
150+
/// The button is visually highlighted (active) when the user is on the homepage.
151+
/// </summary>
152+
[Parameter]
153+
public bool ShowHomeButton { get; set; }
154+
147155
/// <summary>
148156
/// When true, renders a Newsletter subscribe button in the action area.
149157
/// </summary>
@@ -162,13 +170,13 @@ else if (Sections != null && Sections.Any())
162170
[Parameter]
163171
public bool ShowDiscord { get; set; }
164172

165-
/// <summary>
166-
/// When true, renders a Homepage button. Used on the Newsletter page for quick back-navigation.
167-
/// </summary>
168-
[Parameter]
169-
public bool ShowHomepageButton { get; set; }
170-
171-
private bool HasActions => !string.IsNullOrEmpty(RssFeedUrl) || ShowNewsletter;
173+
private bool HasHomepageActions => ShowHomeButton || ShowNewsletter || ShowDiscord || !string.IsNullOrEmpty(RssFeedUrl);
174+
private bool HasHomepageContent => HasHomepageActions || (!HideStats && HomepageStats.TotalCount.HasValue);
175+
private bool ShouldFetchHomepageStats => Section == null
176+
&& Sections != null
177+
&& HasHomepageActions
178+
&& !HideStats
179+
&& !HomepageStats.TotalCount.HasValue;
172180

173181
private bool dropdownOpen;
174182

@@ -274,6 +282,32 @@ else if (Sections != null && Sections.Any())
274282
HomepageStats.OnChanged += OnHomepageStatsChanged;
275283
}
276284

285+
protected override async Task OnParametersSetAsync()
286+
{
287+
if (ShouldFetchHomepageStats)
288+
{
289+
var weekAgoDate = GetWeekAgoDate();
290+
var recentItemsTask = ApiClient.GetCollectionItemsAsync("all", "all", take: 1, fromDate: weekAgoDate);
291+
var allItemsTask = ApiClient.GetCollectionItemsAsync("all", "all", take: 1);
292+
293+
await Task.WhenAll(recentItemsTask, allItemsTask);
294+
295+
var recentCount = (await recentItemsTask)?.TotalCount;
296+
var totalCount = (await allItemsTask)?.TotalCount;
297+
298+
HomepageStats.Update(recentCount, totalCount, weekAgoDate, Sections?.Count() ?? 0);
299+
}
300+
}
301+
302+
private static string GetWeekAgoDate()
303+
{
304+
var brussels = TimeZoneInfo.FindSystemTimeZoneById("Europe/Brussels");
305+
return TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, brussels)
306+
.Date
307+
.AddDays(-7)
308+
.ToString("yyyy-MM-dd");
309+
}
310+
277311
private void OnHomepageStatsChanged()
278312
{
279313
InvokeAsync(StateHasChanged);
@@ -292,6 +326,17 @@ else if (Sections != null && Sections.Any())
292326
HomepageStats.OnChanged -= OnHomepageStatsChanged;
293327
}
294328

329+
/// <summary>
330+
/// Determines if the user is currently on the homepage (path is exactly "/").
331+
/// Used specifically for the Home button active state; avoids the prefix-match
332+
/// in IsActive which would match "/" against every URL.
333+
/// </summary>
334+
private bool IsHomepageActive()
335+
{
336+
var currentPath = new Uri(Navigation.Uri).AbsolutePath.TrimEnd('/');
337+
return string.IsNullOrEmpty(currentPath);
338+
}
339+
295340
/// <summary>
296341
/// Determines if the given URL should be visually highlighted in the subnav.
297342
/// Matches both exact pages and content detail pages under a collection.

src/TechHub.Web/Components/SubNav.razor.css

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ html.keyboard-nav .btn-subnav:focus-visible {
267267
display: flex;
268268
flex-wrap: wrap;
269269
justify-content: center;
270-
gap: var(--spacing-2);
270+
gap: var(--spacing-1);
271271
}
272272

273273
/* Stat buttons: standalone button-style chips for stats */
@@ -289,12 +289,13 @@ html.keyboard-nav .btn-subnav:focus-visible {
289289
}
290290

291291
@media (max-width: 640px) {
292-
/* On small mobile, show only icon + number — hide all label text */
293-
.stat-chip-new-label {
292+
/* On small mobile, keep "new" text and hide "this week" + total chip */
293+
.stat-chip-new-label-suffix,
294+
.stat-chip-total-label {
294295
display: none;
295296
}
296297

297-
.stat-chip-total-label {
298+
.btn-subnav-action.btn-subnav-stat-total {
298299
display: none;
299300
}
300301
}
@@ -329,29 +330,33 @@ html.keyboard-nav .btn-subnav:focus-visible {
329330
}
330331
}
331332

332-
.btn-subnav-newsletter {
333+
.btn-subnav-discord {
333334
border-color: var(--color-purple-bright);
334335
color: var(--color-purple-bright);
335336
}
336337

337338
@media (hover: hover) {
338-
.btn-subnav-newsletter:hover {
339+
.btn-subnav-discord:hover {
339340
background: var(--color-purple-dark);
340341
color: var(--color-text-on-emphasis);
341342
border-color: var(--color-purple-medium);
342343
}
343344
}
344345

345-
.btn-subnav-discord {
346+
/* Newsletter/Home nav button active state — overrides btn-subnav-action secondary color.
347+
.btn-subnav-action is declared later in the file than .btn-subnav.active, so at equal
348+
specificity it would win and reset the color. This rule restores the active appearance. */
349+
.btn-subnav-action.active {
350+
color: var(--color-text-on-emphasis);
351+
background: var(--color-purple-dark);
346352
border-color: var(--color-purple-bright);
347-
color: var(--color-purple-bright);
348353
}
349354

350355
@media (hover: hover) {
351-
.btn-subnav-discord:hover {
352-
background: var(--color-purple-dark);
356+
.btn-subnav-action.active:hover {
357+
background: var(--color-purple-medium);
353358
color: var(--color-text-on-emphasis);
354-
border-color: var(--color-purple-medium);
359+
text-decoration: none;
355360
}
356361
}
357362

0 commit comments

Comments
 (0)