Skip to content

Commit

Permalink
feat: Add Short Codes
Browse files Browse the repository at this point in the history
  • Loading branch information
linkdotnet committed Oct 17, 2024
1 parent bfb8fbf commit e3cebd8
Show file tree
Hide file tree
Showing 24 changed files with 498 additions and 19 deletions.
14 changes: 7 additions & 7 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0-rc.2.24474.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0-rc.2.24474.1" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="9.0.0-rc.2.24474.3" />
<PackageVersion Include="MongoDB.Driver" Version="2.29.0" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="8.0.5" />
<PackageVersion Include="MongoDB.Driver" Version="2.30.0" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="9.0.0-preview" />
<PackageVersion Include="RavenDB.Client" Version="6.2.0" />
</ItemGroup>
<ItemGroup Label="Web">
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="8.0.1" />
<PackageVersion Include="Blazored.Toast" Version="4.2.1" />
<PackageVersion Include="Blazorise.Bootstrap5" Version="1.6.1" />
<PackageVersion Include="Blazorise.Markdown" Version="1.6.1" />
<PackageVersion Include="Blazorise.Bootstrap5" Version="1.6.2" />
<PackageVersion Include="Blazorise.Markdown" Version="1.6.2" />
<PackageVersion Include="Markdig" Version="0.37.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.0-rc.2.24474.3" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.0-rc.2.24473.5" />
<PackageVersion Include="NCronJob" Version="3.0.3" />
<PackageVersion Include="NCronJob" Version="3.1.2-preview" />
<PackageVersion Include="System.ServiceModel.Syndication" Version="9.0.0-rc.2.24473.5" />
</ItemGroup>
<ItemGroup Label="Tests">
Expand All @@ -38,6 +38,6 @@
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
<PackageVersion Include="xunit.v3" Version="0.4.0-pre.20" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0-pre.35" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0-pre.35" />
</ItemGroup>
</Project>
</Project>
15 changes: 15 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
This document describes the changes that need to be made to migrate from one version of the blog to another.

## 8.0 to 9.0

### Shortcodes
Shortcodes, a form a templating that can be adjusted dynamically, are introduced in this version. The following table has to be added to the database:

```sql
CREATE TABLE Shortcodes
(
Id [NVARCHAR](450)] NOT NULL,
Name [NVARCHAR(512)] NOT NULL,
MarkdownContent NVARCHAR(MAX) NOT NULL,
)
```

### Similiar blog posts

A new `SimilarBlogPost` table is introduced to store similar blog posts.

```sql
Expand Down
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This also includes source code snippets. Highlighting is done via [highlight.js]
- [Comments](./docs/Comments/Readme.md)
- [Storage Provider](./docs/Storage/Readme.md)
- [Search Engine Optimization (SEO)](./docs/SEO/Readme.md)
- [Advanced Features](.docs/Features/AdvancedFeatures.md)

## Installation

Expand Down
29 changes: 29 additions & 0 deletions docs/Features/AdvancedFeatures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Advanced Features

This page lists some of the more advanced or less-used features of the blog software.

### Shortcodes
Shortcodes are markdown content that can be shown inside blog posts (like templates that can be referenced).
The idea is to reuse certain shortcodes across various blog posts.
If you update the shortcode, it will be updated across all those blog posts as well.

For example if you have a running promotion you can add a shortcode and link it in various blog posts. Updating the shortcode (for example that it is almost sold out) will update all blog posts that reference this shortcode.

#### Creating a shortcode

To create a shortcode, click on "Shortcodes" in the Admin tab of the navigation bar. You can create a shortcode by adding a name in the top row and the markdown content in the editor. Clicking on an already existing shortcode will allow you to either edit the shortcode or delete it.

Currently, deleting a shortcode will leave the shortcode name inside the blogpost. Therefore only delete shortcodes if you are sure that they are not used anymore.

#### Using a shortcode
There are two ways:
1. If you know the shortcode name, just type in `[[SHORTCODENAME]]` where `SHORTCODENAME` is the name you gave the shortcode.
2. Click on the more button in the editor and select "Shortcodes". This will open a dialog where you can select the shortcode you want to insert and puts it into the clipboard.

### Limitations
Shortcodes
* are not recursive. This means that you cannot use a shortcode inside a shortcode.
* are not part of the table of contents even though they might have headers.
* are not part of the reading time calculation.
* are only available in the content section of a blog post and not the description.
* are currently only copied to the clipboard and not inserted directly into the editor at the cursor position.
28 changes: 28 additions & 0 deletions src/LinkDotNet.Blog.Domain/ShortCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;

namespace LinkDotNet.Blog.Domain;

public class ShortCode : Entity
{
private ShortCode(string name, string markdownContent)
{
Name = name;
MarkdownContent = markdownContent;
}

public string MarkdownContent { get; private set; }

public string Name { get; set; }

public void Update(string name, string content)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
MarkdownContent = content;
Name = name;
}

public static ShortCode Create(string name, string content)
{
return new ShortCode(name, content);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public BlogDbContext(DbContextOptions options)

public DbSet<SimilarBlogPost> SimilarBlogPosts { get; set; }

public DbSet<ShortCode> ShortCodes { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
ArgumentNullException.ThrowIfNull(modelBuilder);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using LinkDotNet.Blog.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace LinkDotNet.Blog.Infrastructure.Persistence.Sql.Mapping;

internal sealed class ShortCodeConfiguration : IEntityTypeConfiguration<ShortCode>
{
public void Configure(EntityTypeBuilder<ShortCode> builder)
{
builder.HasKey(s => s.Id);
builder.Property(s => s.Id)
.IsUnicode(false)
.ValueGeneratedOnAdd();
builder.Property(s => s.Name)
.IsRequired()
.HasMaxLength(512);
builder.Property(s => s.MarkdownContent)
.IsRequired();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</div>
<div class="mb-3">
<label for="talk-content">Description</label>
<MarkdownTextArea id="talk-content" class="form-control" Rows="10"
<MarkdownTextArea id="talk-content" Class="form-control" Rows="10"
@bind-Value="@model.Description"></MarkdownTextArea>
</div>
<button id="talk-submit" class="btn btn-primary" type="submit">Submit</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
@using LinkDotNet.Blog.Domain
@using LinkDotNet.Blog.Infrastructure
@using LinkDotNet.Blog.Infrastructure.Persistence
@using LinkDotNet.Blog.Web.Features.Services
@using NCronJob
@inject IJSRuntime JSRuntime
@inject ICacheInvalidator CacheInvalidator
@inject IInstantJobRegistry InstantJobRegistry
@inject IRepository<ShortCode> ShortCodeRepository

<div class="container">
<h3 class="fw-bold">@Title</h3>
Expand All @@ -17,13 +20,33 @@
</div>
<div class="form-floating mb-3">
<MarkdownTextArea Id="short" Class="form-control" Rows="4" Placeholder="Short Description"
@bind-Value="@model.ShortDescription"></MarkdownTextArea>
@bind-Value="@model.ShortDescription"
PreviewFunction="ReplaceShortCodes"
></MarkdownTextArea>
<ValidationMessage For="() => model.ShortDescription"></ValidationMessage>
</div>
<div class="form-floating mb-3">
<div class="form-floating mb-3 relative">
<MarkdownTextArea Id="content" Class="form-control" Rows="20" Placeholder="Content"
PreviewFunction="ReplaceShortCodes"
@bind-Value="@model.Content"></MarkdownTextArea>
<ValidationMessage For="() => model.Content"></ValidationMessage>

<div class="btn-group position-absolute bottom-0 end-0 m-5 extra-buttons">
<button class="btn btn-primary btn-outlined btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
More
</button>
<ul class="dropdown-menu">
@if (shortCodes.Count > 0)
{
<li>
<button type="button" @onclick="OpenShortCodeDialog" class="dropdown-item">
<span>Get shortcode</span>
</button>
</li>
}
<li><button type="button" class="dropdown-item" @onclick="FeatureDialog.Open">Experimental Features</button></li>
</ul>
</div>
</div>
<div class="form-floating mb-3">
<InputText type="url" class="form-control" id="preview" placeholder="Preview-Url" @bind-Value="model.PreviewImageUrl"/>
Expand Down Expand Up @@ -83,6 +106,7 @@
</div>

<FeatureInfoDialog @ref="FeatureDialog"></FeatureInfoDialog>
<ShortCodeDialog @ref="ShortCodeDialog" ShortCodes="shortCodes"></ShortCodeDialog>

<NavigationLock ConfirmExternalNavigation="@model.IsDirty" OnBeforeInternalNavigation="PreventNavigationWhenDirty"></NavigationLock>
@code {
Expand All @@ -99,14 +123,21 @@
public bool ClearAfterCreated { get; set; } = true;

private FeatureInfoDialog FeatureDialog { get; set; } = default!;
private ShortCodeDialog ShortCodeDialog { get; set; } = default!;

private CreateNewModel model = new();

private bool canSubmit = true;
private IPagedList<ShortCode> shortCodes = PagedList<ShortCode>.Empty;

private bool IsScheduled => model.ScheduledPublishDate.HasValue;

protected override void OnParametersSet()
protected override async Task OnInitializedAsync()
{
shortCodes = await ShortCodeRepository.GetAllAsync();
}

protected override void OnParametersSet()
{
if (BlogPost is null)
{
Expand Down Expand Up @@ -161,4 +192,20 @@
context.PreventNavigation();
}
}

private Task<string> ReplaceShortCodes(string markdown)
{
foreach (var code in shortCodes)
{
markdown = markdown.Replace($"[[{code.Name}]]", code.MarkdownContent);
}

return Task.FromResult(MarkdownConverter.ToMarkupString(markdown).Value);
}

private void OpenShortCodeDialog()
{
ShortCodeDialog.Open();
StateHasChanged();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.extra-buttons {
opacity: 0.25;
}

.extra-buttons:hover {
opacity: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@using LinkDotNet.Blog.Domain
@inject IToastService ToastService
@inject IJSRuntime JSRuntime
<ModalDialog @ref="ModalDialog" Title="Select shortcode">
<div class="modal-body">
<div class="alert alert-info">
<p class="p-2">
Clicking on a shortcode, will put the shortcode into the clipboard.
So you can paste it into the markdown editor. Currently it isn't possible to insert it directly into the editor at the cursor position.
</p>
</div>
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Preview</th>
</tr>
</thead>
<tbody>
@foreach (var shortCode in ShortCodes)
{
<tr @onclick="() => AddShortCode(shortCode)" style="cursor: pointer;">
<td>@shortCode.Name</td>
<td>
<div style="max-height: 100px; overflow-y: auto;">
@MarkdownConverter.ToMarkupString(shortCode.MarkdownContent)
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseDialog">Close</button>
</div>
</ModalDialog>

@code {
private ModalDialog ModalDialog { get; set; } = default!;
[Parameter] public IReadOnlyCollection<ShortCode> ShortCodes { get; set; } = [];

public void Open()
{
ModalDialog.Open();
StateHasChanged();
}

private async Task AddShortCode(ShortCode shortCode)
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", $"[[{shortCode.Name}]]");
ToastService.ShowSuccess($"Shortcode {shortCode.Name} copied to clipboard");
CloseDialog();
}

private void CloseDialog()
{
ModalDialog.Close();
}
}
Loading

0 comments on commit e3cebd8

Please sign in to comment.