From 2584208cd64b1859daf0544fe6d3ebfc291895c2 Mon Sep 17 00:00:00 2001 From: Harry Young Date: Thu, 6 Feb 2025 08:55:26 +0000 Subject: [PATCH] Add grid and card rendering --- .../CareLeavers.Web/CareLeavers.Web.csproj | 1 + .../Contentful/ContentfulEntityResolver.cs | 19 ++++ .../ContentfulRenderers/GDSGridRenderer.cs | 35 ++++++++ .../GDSRazorContentRenderer.cs | 21 +++++ .../Controllers/ContentfulController.cs | 25 ++++-- .../CareLeavers.Web/Models/Content/Card.cs | 20 +++++ .../CareLeavers.Web/Models/Content/Grid.cs | 19 ++++ .../CareLeavers.Web/Models/Content/Page.cs | 27 +++++- .../CareLeavers.Web/Models/Enums/GridType.cs | 15 ++++ .../CareLeavers.Web/Models/Enums/PageType.cs | 7 ++ .../CareLeavers.Web/Models/Enums/PageWidth.cs | 11 +++ src/web/CareLeavers.Web/Program.cs | 3 +- .../Views/Contentful/Page.cshtml | 33 ++++++- .../CareLeavers.Web/Views/Shared/Grid.cshtml | 89 +++++++++++++++++++ .../Grid/AlternatingImageAndText.cshtml | 22 +++++ .../Views/Shared/Grid/Card.cshtml | 17 ++++ .../Views/Shared/_LastUpdated.cshtml | 24 +++++ .../Views/Shared/_Layout.cshtml | 2 + 18 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 src/web/CareLeavers.Web/Contentful/ContentfulEntityResolver.cs create mode 100644 src/web/CareLeavers.Web/ContentfulRenderers/GDSGridRenderer.cs create mode 100644 src/web/CareLeavers.Web/ContentfulRenderers/GDSRazorContentRenderer.cs create mode 100644 src/web/CareLeavers.Web/Models/Content/Card.cs create mode 100644 src/web/CareLeavers.Web/Models/Content/Grid.cs create mode 100644 src/web/CareLeavers.Web/Models/Enums/GridType.cs create mode 100644 src/web/CareLeavers.Web/Models/Enums/PageType.cs create mode 100644 src/web/CareLeavers.Web/Models/Enums/PageWidth.cs create mode 100644 src/web/CareLeavers.Web/Views/Shared/Grid.cshtml create mode 100644 src/web/CareLeavers.Web/Views/Shared/Grid/AlternatingImageAndText.cshtml create mode 100644 src/web/CareLeavers.Web/Views/Shared/Grid/Card.cshtml create mode 100644 src/web/CareLeavers.Web/Views/Shared/_LastUpdated.cshtml diff --git a/src/web/CareLeavers.Web/CareLeavers.Web.csproj b/src/web/CareLeavers.Web/CareLeavers.Web.csproj index affd6f7..11474c9 100644 --- a/src/web/CareLeavers.Web/CareLeavers.Web.csproj +++ b/src/web/CareLeavers.Web/CareLeavers.Web.csproj @@ -21,6 +21,7 @@ + diff --git a/src/web/CareLeavers.Web/Contentful/ContentfulEntityResolver.cs b/src/web/CareLeavers.Web/Contentful/ContentfulEntityResolver.cs new file mode 100644 index 0000000..a938be4 --- /dev/null +++ b/src/web/CareLeavers.Web/Contentful/ContentfulEntityResolver.cs @@ -0,0 +1,19 @@ +using CareLeavers.Web.Models.Content; +using Contentful.Core.Configuration; + +namespace CareLeavers.Web.Contentful; + +public class ContentfulEntityResolver : IContentTypeResolver +{ + private Dictionary _types = new() + { + { Page.ContentType, typeof(Page) }, + { Grid.ContentType, typeof(Grid) }, + { Card.ContentType, typeof(Card) } + }; + + public Type? Resolve(string contentTypeId) + { + return _types.TryGetValue(contentTypeId, out var type) ? type : null; + } +} \ No newline at end of file diff --git a/src/web/CareLeavers.Web/ContentfulRenderers/GDSGridRenderer.cs b/src/web/CareLeavers.Web/ContentfulRenderers/GDSGridRenderer.cs new file mode 100644 index 0000000..12deafe --- /dev/null +++ b/src/web/CareLeavers.Web/ContentfulRenderers/GDSGridRenderer.cs @@ -0,0 +1,35 @@ +using CareLeavers.Web.Models.Content; +using Contentful.Core.Models; + +namespace CareLeavers.Web.ContentfulRenderers; + +public class GDSGridRenderer(IServiceProvider serviceProvider) : GDSRazorContentRenderer(serviceProvider) +{ + public override bool SupportsContent(IContent content) + { + if (content is EntryStructure { NodeType: "embedded-entry-block" } entryStructure) + { + if (entryStructure.Data.Target is Grid) + { + return true; + } + } + + return content is Grid; + } + + public override Task RenderAsync(IContent content) + { + Grid? grid; + if (content is Grid) + { + grid = content as Grid; + } + else + { + grid = (content as EntryStructure)?.Data.Target as Grid; + } + + return RenderToString("Shared/Grid", grid); + } +} \ No newline at end of file diff --git a/src/web/CareLeavers.Web/ContentfulRenderers/GDSRazorContentRenderer.cs b/src/web/CareLeavers.Web/ContentfulRenderers/GDSRazorContentRenderer.cs new file mode 100644 index 0000000..9b171b5 --- /dev/null +++ b/src/web/CareLeavers.Web/ContentfulRenderers/GDSRazorContentRenderer.cs @@ -0,0 +1,21 @@ +using Contentful.AspNetCore.Authoring; +using Contentful.Core.Models; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace CareLeavers.Web.ContentfulRenderers; + +public abstract class GDSRazorContentRenderer( + IServiceProvider serviceProvider) + : RazorContentRenderer( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider) +{ + public override string Render(IContent content) + { + var result = RenderAsync(content); + result.Wait(); + return result.Result; + } +} \ No newline at end of file diff --git a/src/web/CareLeavers.Web/Controllers/ContentfulController.cs b/src/web/CareLeavers.Web/Controllers/ContentfulController.cs index f7a2f2a..3fa5ab7 100644 --- a/src/web/CareLeavers.Web/Controllers/ContentfulController.cs +++ b/src/web/CareLeavers.Web/Controllers/ContentfulController.cs @@ -2,6 +2,7 @@ using System.Text; using System.Xml.Linq; using CareLeavers.Web.Caching; +using CareLeavers.Web.Contentful; using CareLeavers.Web.Models.Content; using Contentful.Core; using Contentful.Core.Configuration; @@ -15,10 +16,19 @@ namespace CareLeavers.Web.Controllers; [Route("/")] -public class ContentfulController( - IDistributedCache distributedCache, - IContentfulClient contentfulClient) : Controller +public class ContentfulController : Controller { + private readonly IDistributedCache _distributedCache; + private readonly IContentfulClient _contentfulClient; + + public ContentfulController(IDistributedCache distributedCache, IContentfulClient contentfulClient) + { + _distributedCache = distributedCache; + _contentfulClient = contentfulClient; + + _contentfulClient.ContentTypeResolver = new ContentfulEntityResolver(); + } + private static readonly JsonSerializerSettings ContentfulSerializerSettings = new() { ContractResolver = new CamelCasePropertyNamesContractResolver @@ -83,13 +93,13 @@ public async Task Sitemap([FromServices] IConfiguration configura return BadRequest(); } - var page = await distributedCache.GetOrSetAsync($"content:sitemap", async () => + var page = await _distributedCache.GetOrSetAsync($"content:sitemap", async () => { var pages = new QueryBuilder() .ContentTypeIs("page") .SelectFields(x => new {x.Slug}); - var pageEntries = await contentfulClient.GetEntries(pages); + var pageEntries = await _contentfulClient.GetEntries(pages); XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9"; @@ -112,14 +122,15 @@ public async Task Sitemap([FromServices] IConfiguration configura private Task GetContentfulPage(string slug) { - return distributedCache.GetOrSetAsync($"content:{slug}", async () => + return _distributedCache.GetOrSetAsync($"content:{slug}", async () => { var pages = new QueryBuilder() .ContentTypeIs(Page.ContentType) .FieldEquals(c => c.Slug, slug) + .Include(10) .Limit(1); - var pageEntries = await contentfulClient.GetEntries(pages); + var pageEntries = await _contentfulClient.GetEntries(pages); return pageEntries.FirstOrDefault(); }); diff --git a/src/web/CareLeavers.Web/Models/Content/Card.cs b/src/web/CareLeavers.Web/Models/Content/Card.cs new file mode 100644 index 0000000..923dc11 --- /dev/null +++ b/src/web/CareLeavers.Web/Models/Content/Card.cs @@ -0,0 +1,20 @@ +using Contentful.Core.Models; + +namespace CareLeavers.Web.Models.Content; + +public class Card : ContentfulContent +{ + public static string ContentType { get; } = "card"; + + public string? Title { get; set; } + + public string? Text { get; set; } + + public Asset? Image { get; set; } + + public Page? Link { get; set; } + + public List Types { get; set; } = []; + + public int Position { get; set; } = 0; +} \ No newline at end of file diff --git a/src/web/CareLeavers.Web/Models/Content/Grid.cs b/src/web/CareLeavers.Web/Models/Content/Grid.cs new file mode 100644 index 0000000..1f79ab6 --- /dev/null +++ b/src/web/CareLeavers.Web/Models/Content/Grid.cs @@ -0,0 +1,19 @@ +using CareLeavers.Web.Models.Enums; +using Contentful.Core.Models; + +namespace CareLeavers.Web.Models.Content; + +public class Grid : ContentfulContent +{ + public static string ContentType { get; } = "grid"; + + public string? Title { get; set; } + + public GridType? GridType { get; set; } + + public bool ShowTitle { get; set; } + + public List? Content { get; set; } + + public string? CssClass { get; set; } = "govuk-grid-column-full"; +} \ No newline at end of file diff --git a/src/web/CareLeavers.Web/Models/Content/Page.cs b/src/web/CareLeavers.Web/Models/Content/Page.cs index dc6adbf..2d039f6 100644 --- a/src/web/CareLeavers.Web/Models/Content/Page.cs +++ b/src/web/CareLeavers.Web/Models/Content/Page.cs @@ -1,3 +1,4 @@ +using CareLeavers.Web.Models.Enums; using Contentful.Core.Models; namespace CareLeavers.Web.Models.Content; @@ -6,9 +7,31 @@ public class Page : ContentfulContent { public static string ContentType { get; } = "page"; + public Page? Parent { get; set; } + + public string? SeoTitle { get; set; } + + public string? SeoDescription { get; set; } + + public Asset? SeoImage { get; set; } + + public PageWidth Width { get; set; } + + public PageType? Type { get; set; } + public string? Title { get; set; } public string? Slug { get; set; } - public Document? Content { get; set; } -} \ No newline at end of file + public bool ShowContentsBlock { get; set; } + + public bool ShowLastUpdated { get; set; } + + public Document? Header { get; set; } + + public Document? Footer { get; set; } + + public Document? MainContent { get; set; } + + public Document? SecondaryContent { get; set; } +} diff --git a/src/web/CareLeavers.Web/Models/Enums/GridType.cs b/src/web/CareLeavers.Web/Models/Enums/GridType.cs new file mode 100644 index 0000000..7847bea --- /dev/null +++ b/src/web/CareLeavers.Web/Models/Enums/GridType.cs @@ -0,0 +1,15 @@ +using System.Runtime.Serialization; + +namespace CareLeavers.Web.Models.Enums; + +public enum GridType +{ + Cards, + [EnumMember(Value = "Alternating Image and Text")] + AlternatingImageAndText, + [EnumMember(Value = "External Links")] + ExternalLinks, + Banner, + [EnumMember(Value = "Small Banner")] + SmallBanner +} \ No newline at end of file diff --git a/src/web/CareLeavers.Web/Models/Enums/PageType.cs b/src/web/CareLeavers.Web/Models/Enums/PageType.cs new file mode 100644 index 0000000..a88f551 --- /dev/null +++ b/src/web/CareLeavers.Web/Models/Enums/PageType.cs @@ -0,0 +1,7 @@ +namespace CareLeavers.Web.Models.Enums; + +public enum PageType +{ + Guide, + Advice +} \ No newline at end of file diff --git a/src/web/CareLeavers.Web/Models/Enums/PageWidth.cs b/src/web/CareLeavers.Web/Models/Enums/PageWidth.cs new file mode 100644 index 0000000..e4c8584 --- /dev/null +++ b/src/web/CareLeavers.Web/Models/Enums/PageWidth.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace CareLeavers.Web.Models.Enums; + +public enum PageWidth +{ + [EnumMember(Value = "Two Thirds")] + TwoThirds, + [EnumMember(Value = "Full Width")] + FullWidth +} \ No newline at end of file diff --git a/src/web/CareLeavers.Web/Program.cs b/src/web/CareLeavers.Web/Program.cs index 41db17d..a760a33 100644 --- a/src/web/CareLeavers.Web/Program.cs +++ b/src/web/CareLeavers.Web/Program.cs @@ -57,7 +57,7 @@ builder.Services.AddHealthChecks(); - builder.Services.AddTransient((c) => + builder.Services.AddTransient(serviceProvider => { var renderer = new HtmlRenderer(new HtmlRendererOptions { @@ -71,6 +71,7 @@ renderer.AddRenderer(new GDSParagraphRenderer(renderer.Renderers)); renderer.AddRenderer(new GDSHeaderRenderer(renderer.Renderers)); renderer.AddRenderer(new GDSAssetRenderer(renderer.Renderers)); + renderer.AddRenderer(new GDSGridRenderer(serviceProvider)); return renderer; }); diff --git a/src/web/CareLeavers.Web/Views/Contentful/Page.cshtml b/src/web/CareLeavers.Web/Views/Contentful/Page.cshtml index 9125b94..2e271de 100644 --- a/src/web/CareLeavers.Web/Views/Contentful/Page.cshtml +++ b/src/web/CareLeavers.Web/Views/Contentful/Page.cshtml @@ -1,8 +1,16 @@ +@using CareLeavers.Web.Models.Enums +@using Microsoft.AspNetCore.Mvc.TagHelpers @model CareLeavers.Web.Models.Content.Page @{ ViewBag.Title = Model.Title; Layout = "_Layout"; + + var pageWidth = Model.Width switch + { + PageWidth.TwoThirds => "govuk-grid-column-two-thirds", + _ => "govuk-grid-column-full" + }; } @section BeforeGDSContent @@ -12,12 +20,33 @@

@Model.Title

+ + @if (Model.Header != null) + { + + } }
-
- +
+ @if (Model.Type.HasValue) + { + @Model.Type + } + @* ShowCOntentBlock *@ + + @if (Model.ShowLastUpdated) + { +
+ + }
+ @if (Model.SecondaryContent != null && Model.Width != PageWidth.FullWidth) + { +
+ +
+ }
\ No newline at end of file diff --git a/src/web/CareLeavers.Web/Views/Shared/Grid.cshtml b/src/web/CareLeavers.Web/Views/Shared/Grid.cshtml new file mode 100644 index 0000000..ef0b25c --- /dev/null +++ b/src/web/CareLeavers.Web/Views/Shared/Grid.cshtml @@ -0,0 +1,89 @@ +@using CareLeavers.Web.Models.Content +@using CareLeavers.Web.Models.Enums +@using Microsoft.AspNetCore.Mvc.TagHelpers +@model CareLeavers.Web.Models.Content.Grid + +@{ + switch (Model.GridType) + { + case GridType.Cards: +
+ @if (Model.ShowTitle) + { +

@Model.Title

+ } + @if (Model.Content != null && Model.Content.Any()) + { +
+ @foreach (var content in Model.Content) + { + var card = (Card)content; + + } +
+ } +
+ break; + + case GridType.AlternatingImageAndText: +
+ @if (Model.ShowTitle) + { +

@Model.Title

+ } + + + @if (Model.Content != null && Model.Content.Any()) + { + var position = -1; + foreach (var content in Model.Content) + { + position++; + var card = (Card)content; + card.Position = position; + + } + } +
+ break; + + @* case GridType.Banner: *@ + @* @if (Model.Content != null && Model.Content.Any()) *@ + @* { *@ + @* *@ + @* } *@ + @* *@ + @* break; *@ + @* case GridType.SmallBanner: *@ + @* @if (Model.Content != null && Model.Content.Any()) *@ + @* { *@ + @* *@ + @* } *@ + @* *@ + @* break; *@ + @* *@ + @* case GridType.ExternalLinks: *@ + @*
*@ + @* *@ + @* @if (Model.ShowTitle) *@ + @* { *@ + @*

@Model.Title

*@ + @* } *@ + @* @if (Model.Content != null) *@ + @* { *@ + @* @foreach (var content in Model.Content) *@ + @* { *@ + @* var externalAgency = (ExternalAgency)content; *@ + @* *@ + @* } *@ + @* } *@ + @*
*@ + @* *@ + @* break; *@ + + } +} diff --git a/src/web/CareLeavers.Web/Views/Shared/Grid/AlternatingImageAndText.cshtml b/src/web/CareLeavers.Web/Views/Shared/Grid/AlternatingImageAndText.cshtml new file mode 100644 index 0000000..4548978 --- /dev/null +++ b/src/web/CareLeavers.Web/Views/Shared/Grid/AlternatingImageAndText.cshtml @@ -0,0 +1,22 @@ +@model CareLeavers.Web.Models.Content.Card + +@{ + var link = Model.Link?.Slug ?? string.Empty; + +
+
+
+ +
+
+
+
+ @Model.Types.FirstOrDefault() +

@Model.Title

+

@Model.Text

+

+

+
+
+
+} \ No newline at end of file diff --git a/src/web/CareLeavers.Web/Views/Shared/Grid/Card.cshtml b/src/web/CareLeavers.Web/Views/Shared/Grid/Card.cshtml new file mode 100644 index 0000000..af8aad4 --- /dev/null +++ b/src/web/CareLeavers.Web/Views/Shared/Grid/Card.cshtml @@ -0,0 +1,17 @@ +@model CareLeavers.Web.Models.Content.Card + +@{ + var link = Model.Link?.Slug ?? string.Empty; +} + + \ No newline at end of file diff --git a/src/web/CareLeavers.Web/Views/Shared/_LastUpdated.cshtml b/src/web/CareLeavers.Web/Views/Shared/_LastUpdated.cshtml new file mode 100644 index 0000000..ccaa927 --- /dev/null +++ b/src/web/CareLeavers.Web/Views/Shared/_LastUpdated.cshtml @@ -0,0 +1,24 @@ +@using System.Globalization +@model Contentful.Core.Models.SystemProperties + +
+ +
diff --git a/src/web/CareLeavers.Web/Views/Shared/_Layout.cshtml b/src/web/CareLeavers.Web/Views/Shared/_Layout.cshtml index a72297d..6d6f656 100644 --- a/src/web/CareLeavers.Web/Views/Shared/_Layout.cshtml +++ b/src/web/CareLeavers.Web/Views/Shared/_Layout.cshtml @@ -34,6 +34,8 @@ @Html.GovUkFrontendStyleImports(appendVersion: true) + +