Skip to content
Merged
67 changes: 67 additions & 0 deletions EssentialCSharp.Web.Tests/SiteMappingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,71 @@ public void FindCSyntaxFundamentalsSanitizedWithAnchorReturnsCorrectSiteMap()
Assert.NotNull(foundSiteMap);
Assert.Equivalent(CSyntaxFundamentalsSiteMapping, foundSiteMap);
}

[Fact]
public void FindPercentComplete_KeyIsNull_ReturnsNull()
{
// Arrange

// Act
string? percent = GetSiteMap().FindPercentComplete(null!);

// Assert
Assert.Null(percent);
}

[Theory]
[InlineData(" ")]
[InlineData("")]
public void FindPercentComplete_KeyIsWhiteSpace_ThrowsArgumentException(string? key)
{
// Arrange

// Act

// Assert
Assert.Throws<ArgumentException>(() =>
{
GetSiteMap().FindPercentComplete(key);
});
}

[Theory]
[InlineData("hello-world", "50.00")]
[InlineData("c-syntax-fundamentals", "100.00")]
public void FindPercentComplete_ValidKey_Success(string? key, string result)
{
// Arrange

// Act
string? percent = GetSiteMap().FindPercentComplete(key);

// Assert
Assert.Equal(result, percent);
}

[Fact]
public void FindPercentComplete_EmptySiteMappings_ReturnsZeroPercent()
{
// Arrange
IList<SiteMapping> siteMappings = new List<SiteMapping>();

// Act
string? percent = siteMappings.FindPercentComplete("test");

// Assert
Assert.Equal("0.00", percent);
}

[Fact]
public void FindPercentComplete_KeyNotFound_ReturnsZeroPercent()
{
// Arrange

// Act
string? percent = GetSiteMap().FindPercentComplete("non-existent-key");

// Assert
Assert.Equal("0.00", percent);
}
}
1 change: 1 addition & 0 deletions EssentialCSharp.Web/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public IActionResult Index()

ViewBag.PageTitle = siteMapping.IndentLevel is 0 ? siteMapping.ChapterTitle + " " + siteMapping.RawHeading : siteMapping.RawHeading;
ViewBag.NextPage = FlipPage(siteMapping!.ChapterNumber, siteMapping.PageNumber, true);
ViewBag.CurrentPageKey = siteMapping.PrimaryKey;
ViewBag.PreviousPage = FlipPage(siteMapping.ChapterNumber, siteMapping.PageNumber, false);
ViewBag.HeadContents = headHtml;
ViewBag.Contents = html;
Expand Down
51 changes: 50 additions & 1 deletion EssentialCSharp.Web/Extensions/SiteMappingListExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace EssentialCSharp.Web.Extensions;
using System.Globalization;

namespace EssentialCSharp.Web.Extensions;

public static class SiteMappingListExtensions
{
Expand All @@ -23,4 +25,51 @@ public static class SiteMappingListExtensions
}
return null;
}
/// <summary>
/// Finds percent complete based on a key.
/// </summary>
/// <param name="siteMappings">IList of SiteMappings</param>
/// <param name="key">The key to search for. If null, returns null.</param>
/// <returns>Returns a formatted double for use as the percent complete.</returns>
public static string? FindPercentComplete(this IList<SiteMapping> siteMappings, string? key)
{
if (key is null)
{
return null;
}
if (key.Trim().Length is 0)
{
throw new ArgumentException("Parameter 'key' cannot be null or whitespace.", nameof(key));
}
int currentMappingCount = 0;
int overallMappingCount = 0;
bool currentPageFound = false;
IEnumerable<IGrouping<int, SiteMapping>> chapterGroupings = siteMappings.GroupBy(x => x.ChapterNumber).OrderBy(g => g.Key);
foreach (IGrouping<int, SiteMapping> chapterGrouping in chapterGroupings)
{
IEnumerable<IGrouping<int, SiteMapping>> pageGroupings = chapterGrouping.GroupBy(x => x.PageNumber).OrderBy(g => g.Key);
foreach (IGrouping<int, SiteMapping> pageGrouping in pageGroupings)
{
foreach (SiteMapping siteMapping in pageGrouping)
{
if (!currentPageFound)
{
currentMappingCount++;
}
overallMappingCount++;
if (siteMapping.PrimaryKey == key)
{
currentPageFound = true;
}
}
}
}
if (overallMappingCount is 0 || !currentPageFound)
{
return "0.00";
}

double result = (double)currentMappingCount / overallMappingCount * 100;
return string.Format(CultureInfo.InvariantCulture, "{0:0.00}", result);
}
}
2 changes: 1 addition & 1 deletion EssentialCSharp.Web/Services/ISiteMappingService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace EssentialCSharp.Web.Services;
namespace EssentialCSharp.Web.Services;

public interface ISiteMappingService
{
Expand Down
3 changes: 1 addition & 2 deletions EssentialCSharp.Web/Services/SiteMappingService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using EssentialCSharp.Web.Models;
using System.Globalization;

namespace EssentialCSharp.Web.Services;

Expand All @@ -12,7 +12,6 @@ public SiteMappingService(IWebHostEnvironment webHostEnvironment)
List<SiteMapping>? siteMappings = System.Text.Json.JsonSerializer.Deserialize<List<SiteMapping>>(File.OpenRead(path)) ?? throw new InvalidOperationException("No table of contents found");
SiteMappings = siteMappings;
}

public IEnumerable<SiteMappingDto> GetTocData()
{
return SiteMappings.GroupBy(x => x.ChapterNumber).OrderBy(x => x.Key).Select(x =>
Expand Down
9 changes: 6 additions & 3 deletions EssentialCSharp.Web/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,12 @@
</div>
</div>

<a v-if="chapterParentPage" :href="chapterParentPage.href" class="menu-chapter-title text-light">
<a v-if="chapterParentPage" :href="chapterParentPage.href" class="page-menu menu-chapter-title text-light">
<span v-cloak>{{chapterParentPage.title}}</span>
</a>
<div class="page-menu menu-progress text-light" v-if="isContentPage">
<span v-cloak>{{percentComplete}}%</span>
</div>

<div class="d-flex align-items-center">
<div class="border-end pe-3 d-none d-md-block">
Expand Down Expand Up @@ -268,8 +271,9 @@
<script>
@{
var tocData = _SiteMappings.GetTocData();
var percentComplete = _SiteMappings.SiteMappings.FindPercentComplete((string) ViewBag.CurrentPageKey);
}

PERCENT_COMPLETE = @Json.Serialize(percentComplete);
PREVIOUS_PAGE = @Json.Serialize(ViewBag.PreviousPage)
NEXT_PAGE = @Json.Serialize(ViewBag.NextPage)
TOC_DATA = @Json.Serialize(tocData)
Expand Down Expand Up @@ -320,6 +324,5 @@
</li>
</template>
<script src="~/js/site.js" type="module" asp-append-version="true"></script>

</body>
</html>
18 changes: 12 additions & 6 deletions EssentialCSharp.Web/wwwroot/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -224,25 +224,31 @@ a:hover {
}
}

.page-menu {
white-space: nowrap;
overflow: hidden;
text-decoration: none;
}

.menu-brand {
font-style: normal;
font-weight: 400;
font-size: 1.5rem;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
margin-left: 5px;
}

.menu-chapter-title {
font-style: normal;
font-weight: 300;
font-size: 1.2rem;
text-decoration: none;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
white-space: nowrap;
}

.menu-progress {
font-style: normal;
font-weight: 200;
font-size: 1rem;
}

.has-tooltip {
Expand Down
11 changes: 10 additions & 1 deletion EssentialCSharp.Web/wwwroot/js/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ const app = createApp({

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

const percentComplete = ref(PERCENT_COMPLETE);

const chapterParentPage = currentPage.find((parent) => parent.level === 0);

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

const isContentPage = computed(() => {
let path = window.location.pathname;
return path !== '/home' && path !== '/guidelines' && path !== '/about' && path !== '/announcements';
});

function filterItem(item, query) {
let matches = normalizeString(item.title).includes(query);
if (item.items && item.items.length) {
Expand Down Expand Up @@ -334,11 +341,13 @@ const app = createApp({
tocData,
expandedTocs,
currentPage,
percentComplete,
chapterParentPage,

searchQuery,
filteredTocData,
enableTocFilter
enableTocFilter,
isContentPage
};
},
});
Expand Down