Skip to content

Commit a48ab3f

Browse files
Feat: Page Indicator (v1) (#667)
## Description Added progress percent to top of page. I want to iterate on this feature with some of the following possible changes: - Perhaps add page number field to Tooling side for the following reasons: - More intuitive to enter page number to navigate to it - Easier to read/remember than a percentage - More accurate - Add ability to navigate to percent/page number - Add same functionality to tooltip as the following issue suggests Fixes IntelliTect-dev/EssentialCSharp.Tooling#442 ### Ensure that your pull request has followed all the steps below: - [x] Code compilation - [x] Created tests which fail without the change (if possible) - [x] All tests passing - [x] Extended the README / documentation, if necessary --------- Co-authored-by: Benjamin Michaelis <[email protected]>
1 parent 6afb357 commit a48ab3f

File tree

8 files changed

+148
-14
lines changed

8 files changed

+148
-14
lines changed

EssentialCSharp.Web.Tests/SiteMappingTests.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,71 @@ public void FindCSyntaxFundamentalsSanitizedWithAnchorReturnsCorrectSiteMap()
8282
Assert.NotNull(foundSiteMap);
8383
Assert.Equivalent(CSyntaxFundamentalsSiteMapping, foundSiteMap);
8484
}
85+
86+
[Fact]
87+
public void FindPercentComplete_KeyIsNull_ReturnsNull()
88+
{
89+
// Arrange
90+
91+
// Act
92+
string? percent = GetSiteMap().FindPercentComplete(null!);
93+
94+
// Assert
95+
Assert.Null(percent);
96+
}
97+
98+
[Theory]
99+
[InlineData(" ")]
100+
[InlineData("")]
101+
public void FindPercentComplete_KeyIsWhiteSpace_ThrowsArgumentException(string? key)
102+
{
103+
// Arrange
104+
105+
// Act
106+
107+
// Assert
108+
Assert.Throws<ArgumentException>(() =>
109+
{
110+
GetSiteMap().FindPercentComplete(key);
111+
});
112+
}
113+
114+
[Theory]
115+
[InlineData("hello-world", "50.00")]
116+
[InlineData("c-syntax-fundamentals", "100.00")]
117+
public void FindPercentComplete_ValidKey_Success(string? key, string result)
118+
{
119+
// Arrange
120+
121+
// Act
122+
string? percent = GetSiteMap().FindPercentComplete(key);
123+
124+
// Assert
125+
Assert.Equal(result, percent);
126+
}
127+
128+
[Fact]
129+
public void FindPercentComplete_EmptySiteMappings_ReturnsZeroPercent()
130+
{
131+
// Arrange
132+
IList<SiteMapping> siteMappings = new List<SiteMapping>();
133+
134+
// Act
135+
string? percent = siteMappings.FindPercentComplete("test");
136+
137+
// Assert
138+
Assert.Equal("0.00", percent);
139+
}
140+
141+
[Fact]
142+
public void FindPercentComplete_KeyNotFound_ReturnsZeroPercent()
143+
{
144+
// Arrange
145+
146+
// Act
147+
string? percent = GetSiteMap().FindPercentComplete("non-existent-key");
148+
149+
// Assert
150+
Assert.Equal("0.00", percent);
151+
}
85152
}

EssentialCSharp.Web/Controllers/HomeController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public IActionResult Index()
3030

3131
ViewBag.PageTitle = siteMapping.IndentLevel is 0 ? siteMapping.ChapterTitle + " " + siteMapping.RawHeading : siteMapping.RawHeading;
3232
ViewBag.NextPage = FlipPage(siteMapping!.ChapterNumber, siteMapping.PageNumber, true);
33+
ViewBag.CurrentPageKey = siteMapping.PrimaryKey;
3334
ViewBag.PreviousPage = FlipPage(siteMapping.ChapterNumber, siteMapping.PageNumber, false);
3435
ViewBag.HeadContents = headHtml;
3536
ViewBag.Contents = html;

EssentialCSharp.Web/Extensions/SiteMappingListExtensions.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace EssentialCSharp.Web.Extensions;
1+
using System.Globalization;
2+
3+
namespace EssentialCSharp.Web.Extensions;
24

35
public static class SiteMappingListExtensions
46
{
@@ -23,4 +25,51 @@ public static class SiteMappingListExtensions
2325
}
2426
return null;
2527
}
28+
/// <summary>
29+
/// Finds percent complete based on a key.
30+
/// </summary>
31+
/// <param name="siteMappings">IList of SiteMappings</param>
32+
/// <param name="key">The key to search for. If null, returns null.</param>
33+
/// <returns>Returns a formatted double for use as the percent complete.</returns>
34+
public static string? FindPercentComplete(this IList<SiteMapping> siteMappings, string? key)
35+
{
36+
if (key is null)
37+
{
38+
return null;
39+
}
40+
if (key.Trim().Length is 0)
41+
{
42+
throw new ArgumentException("Parameter 'key' cannot be null or whitespace.", nameof(key));
43+
}
44+
int currentMappingCount = 0;
45+
int overallMappingCount = 0;
46+
bool currentPageFound = false;
47+
IEnumerable<IGrouping<int, SiteMapping>> chapterGroupings = siteMappings.GroupBy(x => x.ChapterNumber).OrderBy(g => g.Key);
48+
foreach (IGrouping<int, SiteMapping> chapterGrouping in chapterGroupings)
49+
{
50+
IEnumerable<IGrouping<int, SiteMapping>> pageGroupings = chapterGrouping.GroupBy(x => x.PageNumber).OrderBy(g => g.Key);
51+
foreach (IGrouping<int, SiteMapping> pageGrouping in pageGroupings)
52+
{
53+
foreach (SiteMapping siteMapping in pageGrouping)
54+
{
55+
if (!currentPageFound)
56+
{
57+
currentMappingCount++;
58+
}
59+
overallMappingCount++;
60+
if (siteMapping.PrimaryKey == key)
61+
{
62+
currentPageFound = true;
63+
}
64+
}
65+
}
66+
}
67+
if (overallMappingCount is 0 || !currentPageFound)
68+
{
69+
return "0.00";
70+
}
71+
72+
double result = (double)currentMappingCount / overallMappingCount * 100;
73+
return string.Format(CultureInfo.InvariantCulture, "{0:0.00}", result);
74+
}
2675
}

EssentialCSharp.Web/Services/ISiteMappingService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace EssentialCSharp.Web.Services;
1+
namespace EssentialCSharp.Web.Services;
22

33
public interface ISiteMappingService
44
{

EssentialCSharp.Web/Services/SiteMappingService.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using EssentialCSharp.Web.Models;
1+
using System.Globalization;
22

33
namespace EssentialCSharp.Web.Services;
44

@@ -12,7 +12,6 @@ public SiteMappingService(IWebHostEnvironment webHostEnvironment)
1212
List<SiteMapping>? siteMappings = System.Text.Json.JsonSerializer.Deserialize<List<SiteMapping>>(File.OpenRead(path)) ?? throw new InvalidOperationException("No table of contents found");
1313
SiteMappings = siteMappings;
1414
}
15-
1615
public IEnumerable<SiteMappingDto> GetTocData()
1716
{
1817
return SiteMappings.GroupBy(x => x.ChapterNumber).OrderBy(x => x.Key).Select(x =>

EssentialCSharp.Web/Views/Shared/_Layout.cshtml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,12 @@
121121
</div>
122122
</div>
123123

124-
<a v-if="chapterParentPage" :href="chapterParentPage.href" class="menu-chapter-title text-light">
124+
<a v-if="chapterParentPage" :href="chapterParentPage.href" class="page-menu menu-chapter-title text-light">
125125
<span v-cloak>{{chapterParentPage.title}}</span>
126126
</a>
127+
<div class="page-menu menu-progress text-light" v-if="isContentPage">
128+
<span v-cloak>{{percentComplete}}%</span>
129+
</div>
127130

128131
<div class="d-flex align-items-center">
129132
<div class="border-end pe-3 d-none d-md-block">
@@ -268,8 +271,9 @@
268271
<script>
269272
@{
270273
var tocData = _SiteMappings.GetTocData();
274+
var percentComplete = _SiteMappings.SiteMappings.FindPercentComplete((string) ViewBag.CurrentPageKey);
271275
}
272-
276+
PERCENT_COMPLETE = @Json.Serialize(percentComplete);
273277
PREVIOUS_PAGE = @Json.Serialize(ViewBag.PreviousPage)
274278
NEXT_PAGE = @Json.Serialize(ViewBag.NextPage)
275279
TOC_DATA = @Json.Serialize(tocData)
@@ -320,6 +324,5 @@
320324
</li>
321325
</template>
322326
<script src="~/js/site.js" type="module" asp-append-version="true"></script>
323-
324327
</body>
325328
</html>

EssentialCSharp.Web/wwwroot/css/styles.css

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -224,25 +224,31 @@ a:hover {
224224
}
225225
}
226226

227+
.page-menu {
228+
white-space: nowrap;
229+
overflow: hidden;
230+
text-decoration: none;
231+
}
232+
227233
.menu-brand {
228234
font-style: normal;
229235
font-weight: 400;
230236
font-size: 1.5rem;
231-
text-decoration: none;
232-
white-space: nowrap;
233-
overflow: hidden;
234237
margin-left: 5px;
235238
}
236239

237240
.menu-chapter-title {
238241
font-style: normal;
239242
font-weight: 300;
240243
font-size: 1.2rem;
241-
text-decoration: none;
242244
text-overflow: ellipsis;
243-
overflow: hidden;
244245
cursor: pointer;
245-
white-space: nowrap;
246+
}
247+
248+
.menu-progress {
249+
font-style: normal;
250+
font-weight: 200;
251+
font-size: 1rem;
246252
}
247253

248254
.has-tooltip {

EssentialCSharp.Web/wwwroot/js/site.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ const app = createApp({
210210

211211
const currentPage = findCurrentPage([], tocData) ?? [];
212212

213+
const percentComplete = ref(PERCENT_COMPLETE);
214+
213215
const chapterParentPage = currentPage.find((parent) => parent.level === 0);
214216

215217
const sectionTitle = ref(currentPage?.[0]?.title || "Essential C#");
@@ -277,6 +279,11 @@ const app = createApp({
277279
return tocData.filter(item => filterItem(item, query));
278280
});
279281

282+
const isContentPage = computed(() => {
283+
let path = window.location.pathname;
284+
return path !== '/home' && path !== '/guidelines' && path !== '/about' && path !== '/announcements';
285+
});
286+
280287
function filterItem(item, query) {
281288
let matches = normalizeString(item.title).includes(query);
282289
if (item.items && item.items.length) {
@@ -334,11 +341,13 @@ const app = createApp({
334341
tocData,
335342
expandedTocs,
336343
currentPage,
344+
percentComplete,
337345
chapterParentPage,
338346

339347
searchQuery,
340348
filteredTocData,
341-
enableTocFilter
349+
enableTocFilter,
350+
isContentPage
342351
};
343352
},
344353
});

0 commit comments

Comments
 (0)