Make blog slug case insensitive, splitted blog view on page, view and md view, added simplemde md editor, styled markdown alerts

This commit is contained in:
the1mason 2024-09-28 02:52:42 +05:00
parent ce19f164dc
commit 8ffa4a39fe
15 changed files with 298 additions and 137 deletions

View File

@ -26,7 +26,7 @@ public static class DependencyInjection
// infrastructure // infrastructure
services.AddSingleton<IBlogMetaRepository, BlogMetaRepository>(); services.AddSingleton<IBlogMetaRepository, BlogMetaRepository>();
services.AddSingleton<IBlogFileRepository, BlogBlogFileRepository>(); services.AddSingleton<IBlogFileRepository, BlogFileRepository>();
services.AddSingleton<IFileMetaRepository, FileMetaRepository>(); services.AddSingleton<IFileMetaRepository, FileMetaRepository>();
services.AddSingleton<IFileRepository, FileRepository>(); services.AddSingleton<IFileRepository, FileRepository>();

View File

@ -5,7 +5,7 @@ public sealed class BlogMeta
public int? Id { get; init; } public int? Id { get; init; }
public required string Title { get; init; } public required string Title { get; init; }
public required string SourceLocation { get; init; } public required string SourceLocation { get; init; }
public string? Slug { get; init; } public string? Slug { get; set; }
public bool ShowDetails { get; init; } = true; public bool ShowDetails { get; init; } = true;
public string? ImageUrl { get; init; } public string? ImageUrl { get; init; }
public DateTime CreatedAt { get; init; } public DateTime CreatedAt { get; init; }

View File

@ -20,8 +20,5 @@ public class PagedResponse<T>
return new PagedResponse<T>(total, amount, offset, data); return new PagedResponse<T>(total, amount, offset, data);
} }
public static PagedResponse<T> Empty() public static PagedResponse<T> Empty { get; } = new(0, 0, 0, []);
{
return new PagedResponse<T>(0, 0, 0, []);
}
} }

View File

@ -5,7 +5,7 @@ using Microsoft.Extensions.Options;
namespace FastBlog.Core.Repositories; namespace FastBlog.Core.Repositories;
public sealed class BlogBlogFileRepository(IOptionsMonitor<FileStoreOptions> fileOptions) : IBlogFileRepository public sealed class BlogFileRepository(IOptionsMonitor<FileStoreOptions> fileOptions) : IBlogFileRepository
{ {
public async Task<string?> Read(string path) public async Task<string?> Read(string path)
{ {

View File

@ -1,6 +1,4 @@
using System.Diagnostics; using FastBlog.Core.Abstractions.Repositories.Blogs;
using FastBlog.Core.Abstractions.Repositories.Blogs;
using FastBlog.Core.Abstractions.Repositories.Files;
using FastBlog.Core.Models; using FastBlog.Core.Models;
using FastBlog.Core.Models.Blogs; using FastBlog.Core.Models.Blogs;
@ -10,6 +8,7 @@ public sealed class BlogService(IBlogMetaRepository metaRepository, IBlogFileRep
{ {
public async Task<Blog?> Get(string? slug) public async Task<Blog?> Get(string? slug)
{ {
slug = slug?.ToLower();
var metaResult = await metaRepository.Get(slug); var metaResult = await metaRepository.Get(slug);
if (metaResult is null) if (metaResult is null)
return null; return null;
@ -49,6 +48,7 @@ public sealed class BlogService(IBlogMetaRepository metaRepository, IBlogFileRep
public Task<Result<Blog, BusinessError>> Update(Blog blog) public Task<Result<Blog, BusinessError>> Update(Blog blog)
{ {
blog.Metadata.Slug = blog.Metadata.Slug?.ToLower();
return blog.Metadata.Id is null ? Create(blog) : UpdateInternal(blog); return blog.Metadata.Id is null ? Create(blog) : UpdateInternal(blog);
} }

View File

@ -123,7 +123,7 @@ public class BlogsController(BlogService service) : Controller
[Route("preview")] [Route("preview")]
public IActionResult Preview([FromForm] EditBlog editBlog) public IActionResult Preview([FromForm] EditBlog editBlog)
{ {
return View("Index", new Blog return View("Preview", new Blog
{ {
Text = editBlog.Text, Text = editBlog.Text,
Metadata = new() Metadata = new()

View File

@ -1,9 +1,10 @@
@model EditBlog @model EditBlog
<div id="preview"> <link rel="stylesheet" href="/lib/simplemde/simplemde.min.css">
</div> <div class="grid">
<div>
@if (Model.Id is null) @if (Model.Id is null)
{ {
<h2>New Blog</h2> <h2>New Blog</h2>
@ -37,11 +38,13 @@ else
</div> </div>
<div> <div>
<label for="createdAt">Created At:</label> <label for="createdAt">Created At:</label>
<input type="datetime-local" id="createdAt" name="createdAt" value="@Model.CreatedAt.ToString("yyyy-MM-ddTHH:mm")" required> <input type="datetime-local" id="createdAt" name="createdAt"
value="@Model.CreatedAt.ToString("yyyy-MM-ddTHH:mm")" required>
</div> </div>
<div> <div>
<label for="modifiedAt">Modified At:</label> <label for="modifiedAt">Modified At:</label>
<input type="datetime-local" id="modifiedAt" name="modifiedAt" value="@Model.ModifiedAt.ToString("yyyy-MM-ddTHH:mm")" required> <input type="datetime-local" id="modifiedAt" name="modifiedAt"
value="@Model.ModifiedAt.ToString("yyyy-MM-ddTHH:mm")" required>
</div> </div>
<div> <div>
<label for="signature">Signature:</label> <label for="signature">Signature:</label>
@ -60,8 +63,9 @@ else
<label for="text">Text:</label> <label for="text">Text:</label>
<textarea style="field-sizing: content" id="text" name="text" required>@Model.Text</textarea> <textarea style="field-sizing: content" id="text" name="text" required>@Model.Text</textarea>
</div> </div>
<div class="grid">
<div> <div>
<button type="submit" class="create-btn"> <button type="submit">
@if (Model.Id is null) @if (Model.Id is null)
{ {
<span>Create</span> <span>Create</span>
@ -73,9 +77,60 @@ else
</button> </button>
</div> </div>
<div> <div>
<button class="preview-btn secondary" hx-post="/blogs/preview" hx-target="#preview" hx-swap="OuterHTML">Preview</button> <button class="secondary btn-fw" hx-post="/blogs/preview" hx-target="#preview" hx-swap="OuterHTML">
Preview
</button>
</div>
</div> </div>
</form> </form>
</div>
<div id="preview">
</div>
</div>
<style>
.CodeMirror {
background: var(--pico-form-element-background-color);
border-color: var(--pico-form-element-border-color);
color: var(--pico-form-element-color);
border-radius: var(--pico-border-radius);
}
.CodeMirror .CodeMirror-selected {
background: var(--pico-text-selection-color);
}
.CodeMirror-focused .CodeMirror-selected, .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection {
background: var(--pico-text-selection-color);
}
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection {
background: var(--pico-text-selection-color);
}
.CodeMirror-cursor {
border-color: white;
}
.CodeMirror:active {
background: var(--pico-form-element-active-background-color);
border-color: var(--pico-form-element-active-border-color);
}
</style>
<script src="/lib/simplemde/simplemde.min.js"></script>
<script>
let simplemde = new SimpleMDE({
element: document.getElementById("text"),
toolbar: false,
forceSync: true,
renderingConfig: {
codeSyntaxHighlighting: false,
},
spellChecker: false,
styleSelectedText: false
});
</script>

View File

@ -1,53 +1,10 @@
@using Ganss.Xss @model FastBlog.Core.Models.Blogs.Blog
@using Markdig;
@using Markdig.Extensions.AutoIdentifiers
@using Pek.Markdig.HighlightJs
@model FastBlog.Core.Models.Blogs.Blog
@{ @{
ViewBag.Title = Model.Metadata.Title; ViewBag.Title = Model.Metadata.Title;
var pipelineBuilder = new MarkdownPipelineBuilder()
.UseSmartyPants()
.UseEmojiAndSmiley()
.UseAlertBlocks()
.UseAdvancedExtensions();
pipelineBuilder.Extensions.Remove(pipelineBuilder.Extensions.Find<AutoIdentifierExtension>()!);
pipelineBuilder.UseAutoIdentifiers(AutoIdentifierOptions.GitHub);
pipelineBuilder.UseHighlightJs();
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedAttributes.Add("class");
sanitizer.AllowedAttributes.Add("id");
var pipeline = pipelineBuilder.Build();
} }
<div class="@(Model.Metadata.FullWidth ? "fw" : "container") markdown"> @await Html.PartialAsync("Blogs/BlogContent", Model)
@if (Model.Metadata.ShowDetails)
{
<hr/>
@if (Model.Metadata.ImageUrl is not null)
{
<img src="@Model.Metadata.ImageUrl" alt="@Model.Metadata.Title" class="blog-image"/>
<hr/>
}
<div class="blog-details">
<h1>@Model.Metadata.Title</h1>
@if (Model.Metadata.Signature is not null)
{
<h3>
<span style="opacity: 0.4">by</span> @Model.Metadata.Signature
</h3>
}
<p>@($"{Model.Metadata.CreatedAt:MMMM dd, yyyy}, updated {Model.Metadata.ModifiedAt:MMMM dd, yyyy}")</p>
</div>
<hr/>
<br/>
}
@Html.Raw(sanitizer.Sanitize(Markdown.ToHtml(Model.Text, pipeline)))
</div>
<br> <br>

View File

@ -1,9 +1,7 @@
@model FastBlog.Core.Models.Blogs.Blog @model FastBlog.Core.Models.Blogs.Blog
@await Html.PartialAsync("Index", Model); @await Html.PartialAsync("Blogs/BlogContent", Model)
<hr/> <hr/>
<br/> <br/>
<br/>
<br/>

View File

@ -0,0 +1,29 @@
@model FastBlog.Core.Models.Blogs.Blog
<div class="@(Model.Metadata.FullWidth ? "fw" : "container") markdown">
@if (Model.Metadata.ShowDetails)
{
<hr/>
@if (Model.Metadata.ImageUrl is not null)
{
<img src="@Model.Metadata.ImageUrl" alt="@Model.Metadata.Title" class="blog-image"/>
<hr/>
}
<div class="blog-details">
<h1>@Model.Metadata.Title</h1>
@if (Model.Metadata.Signature is not null)
{
<h3>
<span style="opacity: 0.4">by</span> @Model.Metadata.Signature
</h3>
}
<p>@($"{Model.Metadata.CreatedAt:MMMM dd, yyyy}, updated {Model.Metadata.ModifiedAt:MMMM dd, yyyy}")</p>
</div>
<hr/>
<br/>
}
@await Html.PartialAsync("Markdown", Model.Text)
</div>

View File

@ -0,0 +1,23 @@
@using Ganss.Xss
@using Markdig
@using Markdig.Extensions.AutoIdentifiers
@using Pek.Markdig.HighlightJs
@model string
@{
var pipelineBuilder = new MarkdownPipelineBuilder()
.UseSmartyPants()
.UseEmojiAndSmiley()
.UseAlertBlocks()
.UseAdvancedExtensions();
pipelineBuilder.Extensions.Remove(pipelineBuilder.Extensions.Find<AutoIdentifierExtension>()!);
pipelineBuilder.UseAutoIdentifiers(AutoIdentifierOptions.GitHub);
pipelineBuilder.UseHighlightJs();
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedAttributes.Add("class");
sanitizer.AllowedAttributes.Add("id");
var pipeline = pipelineBuilder.Build();
}
@Html.Raw(sanitizer.Sanitize(Markdown.ToHtml(Model, pipeline)))

View File

@ -32,6 +32,7 @@ return;
<li><a href="~/"><h1>@options.Value.Title</h1></a></li> <li><a href="~/"><h1>@options.Value.Title</h1></a></li>
<li><a href="~/blog/list">Blog</a></li> <li><a href="~/blog/list">Blog</a></li>
<li><a href="~/edit">New</a></li> <li><a href="~/edit">New</a></li>
<li><a href="~/files">Files</a></li>
</ul> </ul>
<ul> <ul>
@foreach (var link in options.Value.Links) @foreach (var link in options.Value.Links)

View File

@ -28,7 +28,35 @@
--pico-card-sectioning-background-color: #17273a; --pico-card-sectioning-background-color: #17273a;
--pico-form-element-background-color: #ffffff08; --pico-form-element-background-color: #ffffff08;
--pico-form-element-active-background-color: #ffffff18; --pico-form-element-active-background-color: #ffffff0D;
--pico-tip: #c5ffc3;
--pico-tip-background: rgba(197, 255, 195, 0.16);
--pico-tip-underline: rgba(197, 255, 195, 0.25);
--pico-tip-hover: #c5ffc3;
--pico-tip-hover-background: rgba(197, 255, 195, 0.40);
--pico-tip-focus: rgba(197, 255, 195, 0.90);
--pico-important: rgb(235, 195, 255);
--pico-important-background: rgba(235, 195, 255, 0.16);
--pico-important-underline: rgba(235, 195, 255, 0.25);
--pico-important-hover: rgb(235, 195, 255);
--pico-important-hover-background: rgba(235, 195, 255, 0.40);
--pico-important-focus: rgba(235, 195, 255, 0.90);
--pico-warning: rgb(255, 137, 55);
--pico-warning-background: rgba(255, 137, 55, 0.16);
--pico-warning-underline: rgba(255, 137, 55, 0.25);
--pico-warning-hover: rgb(255, 137, 55);
--pico-warning-hover-background: rgba(255, 137, 55, 0.40);
--pico-warning-focus: rgba(255, 137, 55, 0.90);
--pico-danger: rgb(255, 55, 55);
--pico-danger-background: rgba(255, 55, 55, 0.16);
--pico-danger-underline: rgba(255, 55, 55, 0.25);
--pico-danger-hover: rgb(255, 55, 55);
--pico-danger-hover-background: rgba(255, 55, 55, 0.40);
--pico-danger-focus: rgba(255, 55, 55, 0.90);
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
@ -103,13 +131,64 @@ body {
margin-top: 35px; margin-top: 35px;
} }
.markdown-alert {
color: inherit;
background-color: inherit;
border-radius: var(--pico-border-radius);
border: 1px solid;
border-left: 5px solid;
padding: 10px;
margin-bottom: 10px;
}
.markdown-alert p {
margin: 0;
}
.markdown-alert-title {
font-family: 'Fira Code', Roboto, sans-serif;
font-size: 16px;
font-weight: bold;
}
.markdown-alert-note {
color: var(--pico-primary);
background: var(--pico-primary-background);
border-color: var(--pico-primary-background);
}
.markdown-alert-tip {
color: var(--pico-tip);
background: var(--pico-tip-background);
border-color: var(--pico-tip-background);
}
.markdown-alert-important {
color: var(--pico-important);
background: var(--pico-important-background);
border-color: var(--pico-important-background);
}
.markdown-alert-warning {
color: var(--pico-warning);
background: var(--pico-warning-background);
border-color: var(--pico-warning-background);
}
.markdown-alert-caution {
color: var(--pico-danger);
background: var(--pico-danger-background);
border-color: var(--pico-danger-background);
}
.blog-image { .blog-image {
display: block; display: block;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
max-height: 30rem; max-height: 30rem;
border: 7px solid rgba(0, 0, 0, 0); border: 7px solid rgba(0, 0, 0, 0);
border-radius: 0; box-shadow: 0 0 0 5px #C3EAFF; border-radius: 0;
box-shadow: 0 0 0 5px #C3EAFF;
} }
.btn-fw { .btn-fw {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long