Add home hero with intro and buttons from the body, next-event card and page tiles
This commit is contained in:
1 parent
b737f57ef7
commit
e51afeb34b
23 files changed
+1262
-65
No files matched your search
@@ -0,0 +1,128 @@
|
||||
using Elternbeirat.Web.Shared;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Elternbeirat.Web.Features.Home;
|
||||
|
||||
/// <summary>
|
||||
/// The head of the home page, taken apart from the <c>home</c> page's Markdown body:
|
||||
/// a large heading, an intro and a row of buttons.
|
||||
/// </summary>
|
||||
/// <param name="Title">The large heading.</param>
|
||||
/// <param name="IntroHtml">The intro rendered to HTML, or an empty string.</param>
|
||||
/// <param name="Links">The buttons, in the order the editor wrote them; may be empty.</param>
|
||||
/// <remarks>
|
||||
/// Everything visible here is written by editors in PocketBase. The page only gives
|
||||
/// it the hero layout, so the editor needs no new field and no special syntax:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>The first <c># heading</c> becomes the large heading.</description></item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// A last paragraph made of nothing but links becomes the button row;
|
||||
/// the first link is the primary button.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item><description>Everything else is the intro text.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// <see cref="Fallback"/> keeps a static heading when PocketBase cannot be
|
||||
/// reached, so the start page never renders without a title.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// HomeIntro.Parse("# Willkommen\n\nSchön, dass Sie da sind.\n\n[Termine](/events) [Kontakt](/contact)");
|
||||
/// // Title "Willkommen", IntroHtml "<p>Schön, dass Sie da sind.</p>\n", two links
|
||||
/// </code>
|
||||
/// </example>
|
||||
public sealed record HomeIntro(string Title, string IntroHtml, IReadOnlyList<HomeLink> Links)
|
||||
{
|
||||
/// <summary>
|
||||
/// The heading shown when the body has none or PocketBase is not reachable.
|
||||
/// </summary>
|
||||
public const string FallbackTitle = "Elternbeirat der IGMH";
|
||||
|
||||
/// <summary>
|
||||
/// The head of the home page without any content from PocketBase: the static
|
||||
/// heading, no intro, no buttons.
|
||||
/// </summary>
|
||||
public static HomeIntro Fallback { get; } = new(FallbackTitle, "", []);
|
||||
|
||||
/// <summary>
|
||||
/// Takes the <c>home</c> page's body apart into heading, intro and buttons.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The body; may be <see langword="null"/> or empty.</param>
|
||||
/// <returns>
|
||||
/// The parts; <see cref="Fallback"/> for an empty body. Without a level-1
|
||||
/// heading, the title is <see cref="FallbackTitle"/> and the rest is used as is.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Only top-level blocks are looked at: a heading or a link paragraph inside a
|
||||
/// list or a <c>:::</c> block stays part of the intro. Links in the button row
|
||||
/// may be separated by spaces or line breaks; any other text in the paragraph
|
||||
/// (e.g. "oder") keeps it a normal paragraph, because the button row would drop
|
||||
/// that text.
|
||||
/// </remarks>
|
||||
public static HomeIntro Parse(string? markdown)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(markdown))
|
||||
{
|
||||
return Fallback;
|
||||
}
|
||||
|
||||
var document = Markdown.Parse(markdown);
|
||||
|
||||
var heading = document.OfType<HeadingBlock>().FirstOrDefault(block => block.Level == 1);
|
||||
if (heading is not null)
|
||||
{
|
||||
document.Remove(heading);
|
||||
}
|
||||
|
||||
var links = document.LastChild is ParagraphBlock last ? LinksOnly(last) : [];
|
||||
if (links.Count > 0)
|
||||
{
|
||||
document.RemoveAt(document.Count - 1);
|
||||
}
|
||||
|
||||
var title = heading?.Inline is { } inline ? Markdown.PlainText(inline) : "";
|
||||
return new HomeIntro(
|
||||
string.IsNullOrEmpty(title) ? FallbackTitle : title,
|
||||
Markdown.Render(document),
|
||||
links);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the links of a paragraph that holds nothing but links.
|
||||
/// </summary>
|
||||
/// <param name="paragraph">The paragraph to inspect.</param>
|
||||
/// <returns>
|
||||
/// Its links, or an empty list if the paragraph contains anything besides links
|
||||
/// and whitespace (including an image).
|
||||
/// </returns>
|
||||
private static List<HomeLink> LinksOnly(ParagraphBlock paragraph) =>
|
||||
paragraph.Inline?.Where(inline => !Markdown.IsBlank(inline)).ToList() is { Count: > 0 } inlines
|
||||
&& inlines.All(inline => inline is LinkInline { IsImage: false } or AutolinkInline)
|
||||
? [.. inlines.Select(ToLink)]
|
||||
: [];
|
||||
|
||||
/// <summary>
|
||||
/// Turns a link inline into a button.
|
||||
/// </summary>
|
||||
/// <param name="inline">A <see cref="LinkInline"/> or <see cref="AutolinkInline"/>.</param>
|
||||
/// <returns>The button's target and visible text.</returns>
|
||||
/// <exception cref="ArgumentException"><paramref name="inline"/> is neither kind of link.</exception>
|
||||
private static HomeLink ToLink(Inline inline) =>
|
||||
inline switch
|
||||
{
|
||||
LinkInline link => new HomeLink(link.Url ?? "", Markdown.PlainText(link)),
|
||||
AutolinkInline autolink => new HomeLink(autolink.Url, autolink.Url),
|
||||
_ => throw new ArgumentException("Not a link.", nameof(inline)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A button in the head of the home page.
|
||||
/// </summary>
|
||||
/// <param name="Href">The link target as the editor wrote it.</param>
|
||||
/// <param name="Text">The visible button text.</param>
|
||||
public sealed record HomeLink(string Href, string Text);
|
||||
Reference in new issue
Block a user