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:
parent
ce19f164dc
commit
8ffa4a39fe
|
|
@ -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>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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, []);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/>
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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)))
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Reference in New Issue