Updated admin page to be more streamlined and added the beginning of the blogging features
This commit is contained in:
198
OpenArchival.Blazor.Blog/BlogEditor.razor
Normal file
198
OpenArchival.Blazor.Blog/BlogEditor.razor
Normal file
@@ -0,0 +1,198 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using OpenArchival.Blazor.AdminPages
|
||||
@using OpenArchival.DataAccess
|
||||
@using OpenArchival.Blazor.CustomComponents
|
||||
@using MudExRichTextEditor
|
||||
@using MudBlazor
|
||||
@using System.Text
|
||||
|
||||
@page "/admin/blogedit"
|
||||
@page "/admin/blogedit/{Id}"
|
||||
@layout AdminPages.AdminControlPanelLayout;
|
||||
|
||||
<MudText Typo="Typo.h6">Create a Blog Post</MudText>
|
||||
<MudDivider></MudDivider>
|
||||
<MudTextField Placeholder="Title" T="string" @bind-Value=@BlogPostModel.Title></MudTextField>
|
||||
|
||||
<MudExRichTextEdit @ref="@_contentEditor"
|
||||
ReadOnly="false"
|
||||
Height="444"
|
||||
Class="m-2 mt-4"
|
||||
Placeholder="Edit html"
|
||||
ValueHtmlBehavior="MudExRichTextEditor.Types.GetHtmlBehavior.SemanticHtml"
|
||||
@bind-Value="BlogPostModel.Content">
|
||||
@((MarkupString)BlogPostModel.Content)
|
||||
</MudExRichTextEdit>
|
||||
|
||||
<ChipContainer T="BlogPostTag" @ref="@_tagsChipContainer" @bind-Items="BlogPostModel.Tags">
|
||||
<InputContent>
|
||||
<MudAutocomplete T="string"
|
||||
Placeholder="Add Tags..."
|
||||
@bind-Text=_tagsInputValue
|
||||
OnKeyDown="HandleTagsEnter"
|
||||
>
|
||||
</MudAutocomplete>
|
||||
</InputContent>
|
||||
</ChipContainer>
|
||||
|
||||
<MudGrid Justify="Justify.FlexEnd" Class="mr-4 pt-8">
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Publish" Color=Color.Primary Variant=Variant.Filled OnClick="OnPublishBlogPost">Publish</MudButton>
|
||||
</MudGrid>
|
||||
|
||||
@inject IDbContextFactory<ApplicationDbContext> ContextFactory;
|
||||
@inject ISnackbar Snackbar;
|
||||
@inject NavigationManager NavigationManager;
|
||||
|
||||
@*OnKeyDown="@(ev => HandleChipContainerEnter<string>(ev, _tagsChipContainer, _tagsInputValue, () => _tagsInputValue = string.Empty))"*@
|
||||
@code {
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
public List<string> StringTags { get; set; } = [];
|
||||
public BlogPost BlogPostModel { get; set; } = new();
|
||||
|
||||
private MudExRichTextEdit _contentEditor = default!;
|
||||
|
||||
private string _tagsInputValue = "";
|
||||
private ChipContainer<BlogPostTag> _tagsChipContainer = default!;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (Id != 0)
|
||||
{
|
||||
using var context = ContextFactory.CreateDbContext();
|
||||
|
||||
BlogPost? existingPost = context.BlogPosts.Include(p => p.Tags).Where(p => p.Id == Id).FirstOrDefault();
|
||||
|
||||
if (existingPost is null)
|
||||
{
|
||||
NavigationManager.NavigateTo($"blog-not-found/{System.Web.HttpUtility.UrlEncode($"No blog with id {Id} exists in the database!")}");
|
||||
return;
|
||||
}
|
||||
|
||||
BlogPostModel = existingPost;
|
||||
}
|
||||
base.OnParametersSet();
|
||||
}
|
||||
|
||||
private async Task DeDuplicatePostTags(BlogPost post, ApplicationDbContext context)
|
||||
{
|
||||
List<BlogPostTag> finalTags = [];
|
||||
foreach (BlogPostTag tag in post.Tags)
|
||||
{
|
||||
var existing = await context.BlogPostTags.Where(t => t.Name == tag.Name).FirstOrDefaultAsync();
|
||||
if (existing is not null)
|
||||
{
|
||||
finalTags.Add(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.BlogPostTags.Add(tag);
|
||||
finalTags.Add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
post.Tags = finalTags;
|
||||
}
|
||||
|
||||
private void BuildBlogSearchIndex(BlogPost post)
|
||||
{
|
||||
var tagsSb = new StringBuilder();
|
||||
var allSb = new StringBuilder();
|
||||
|
||||
allSb.Append($"{post.Title} ");
|
||||
allSb.Append($"{post.Content} ");
|
||||
|
||||
foreach (BlogPostTag tag in post.Tags)
|
||||
{
|
||||
tagsSb.Append($"{tag.Name} ");
|
||||
allSb.Append($"{tag.Name} ");
|
||||
}
|
||||
|
||||
post.AllSearchString = allSb.ToString();
|
||||
post.TagsSearchString = tagsSb.ToString();
|
||||
}
|
||||
|
||||
private async Task OnPublishBlogPost(Microsoft.AspNetCore.Components.Web.MouseEventArgs args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(BlogPostModel.Content))
|
||||
{
|
||||
Snackbar.Add("Content is required in the blog post, write something.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(BlogPostModel.Title))
|
||||
{
|
||||
Snackbar.Add("A post title is required", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||
BlogPostModel.ModifiedTime = DateTime.UtcNow;
|
||||
|
||||
// Blog post does not exist in database
|
||||
if (BlogPostModel.Id == 0)
|
||||
{
|
||||
BlogPostModel.CreationTime = DateTime.UtcNow;
|
||||
|
||||
// Loop through the tags and de-duplicate any of them
|
||||
await DeDuplicatePostTags(BlogPostModel, context);
|
||||
|
||||
BuildBlogSearchIndex(BlogPostModel);
|
||||
|
||||
context.BlogPosts.Add(BlogPostModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Blog post exists in database
|
||||
|
||||
// Find the existing post so we can update it
|
||||
|
||||
BlogPost? existing = await context.BlogPosts.Include(p=>p.Tags).FirstOrDefaultAsync(p => p.Id == BlogPostModel.Id);
|
||||
if (existing is null)
|
||||
{
|
||||
Snackbar.Add($"Unable to find a post with id {BlogPostModel.Id} in the database, cannot update post!", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// De-duplicate tags
|
||||
await DeDuplicatePostTags(BlogPostModel, context);
|
||||
|
||||
// Copy over the values we want
|
||||
existing.Title = BlogPostModel.Title;
|
||||
existing.Content = BlogPostModel.Content;
|
||||
existing.ModifiedTime = BlogPostModel.ModifiedTime;
|
||||
|
||||
existing.Tags.Clear();
|
||||
foreach (var tag in BlogPostModel.Tags)
|
||||
{
|
||||
existing.Tags.Add(tag);
|
||||
}
|
||||
|
||||
BuildBlogSearchIndex(existing);
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Snackbar.Add("Changes saved!", Severity.Success);
|
||||
|
||||
// Reset the model
|
||||
BlogPostModel = new();
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void HandleTagsEnter(KeyboardEventArgs args)
|
||||
{
|
||||
if (args.Key == "Enter")
|
||||
{
|
||||
var newTag = new BlogPostTag() { Name = _tagsInputValue };
|
||||
_tagsInputValue = "";
|
||||
|
||||
BlogPostModel.Tags.Add(newTag);
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
OpenArchival.Blazor.Blog/BlogPostNotFound.razor
Normal file
17
OpenArchival.Blazor.Blog/BlogPostNotFound.razor
Normal file
@@ -0,0 +1,17 @@
|
||||
@namespace OpenArchival.Blazor.Blog
|
||||
|
||||
@using MudBlazor
|
||||
@page "/blog-not-found"
|
||||
@page "/blog-not-found/{Message}"
|
||||
|
||||
<MudPaper Class="pa-4 ma-2 rounded" Elevation="3">
|
||||
<MudText Typo="Typo.h1" Align="Align.Center">404: Post Not Found!</MudText>
|
||||
@if (!string.IsNullOrEmpty(Message)) {
|
||||
<MudText Typo="Typo.body1" Align="Align.Center">@Message</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
28
OpenArchival.Blazor.Blog/OpenArchival.Blazor.Blog.csproj
Normal file
28
OpenArchival.Blazor.Blog/OpenArchival.Blazor.Blog.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MudBlazor" Version="8.13.0" />
|
||||
<PackageReference Include="MudExRichTextEditor" Version="8.13.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenArchival.Blazor.AdminPages\OpenArchival.Blazor.AdminPages.csproj" />
|
||||
<ProjectReference Include="..\OpenArchival.Blazor.Config\OpenArchival.Blazor.Config.csproj" />
|
||||
<ProjectReference Include="..\OpenArchival.Blazor.CustomComponents\OpenArchival.Blazor.CustomComponents.csproj" />
|
||||
<ProjectReference Include="..\OpenArchival.DataAccess\OpenArchival.DataAccess.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user