Compare commits

..

3 Commits

10 changed files with 68 additions and 130 deletions

View File

@ -1,96 +0,0 @@
using Humanizer;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Blog.Data;
using OliverBooth.Blog.Services;
namespace OliverBooth.Blog.Controllers;
/// <summary>
/// Represents a controller for the blog API.
/// </summary>
[ApiController]
[Route("api")]
[Produces("application/json")]
[EnableCors("OliverBooth")]
public sealed class BlogApiController : ControllerBase
{
private readonly IBlogPostService _blogPostService;
private readonly IUserService _userService;
/// <summary>
/// Initializes a new instance of the <see cref="BlogApiController" /> class.
/// </summary>
/// <param name="blogPostService">The <see cref="IBlogPostService" />.</param>
/// <param name="userService">The <see cref="IUserService" />.</param>
public BlogApiController(IBlogPostService blogPostService, IUserService userService)
{
_blogPostService = blogPostService;
_userService = userService;
}
[Route("count")]
public IActionResult Count()
{
if (!ValidateReferer()) return NotFound();
return Ok(new { count = _blogPostService.GetAllBlogPosts().Count });
}
[HttpGet("all/{skip:int?}/{take:int?}")]
public IActionResult GetAllBlogPosts(int skip = 0, int take = -1)
{
if (!ValidateReferer()) return NotFound();
// TODO yes I'm aware I can use the new pagination I wrote, this will be added soon.
IReadOnlyList<IBlogPost> allPosts = _blogPostService.GetAllBlogPosts();
if (take == -1)
{
take = allPosts.Count;
}
return Ok(allPosts.Skip(skip).Take(take).Select(post => new
{
id = post.Id,
commentsEnabled = post.EnableComments,
identifier = post.GetDisqusIdentifier(),
author = post.Author.Id,
title = post.Title,
published = post.Published.ToUnixTimeSeconds(),
formattedDate = post.Published.ToString("dddd, d MMMM yyyy HH:mm"),
updated = post.Updated?.ToUnixTimeSeconds(),
humanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(),
excerpt = _blogPostService.RenderExcerpt(post, out bool trimmed),
trimmed,
url = Url.Page("/Article",
new
{
area = "blog",
year = post.Published.ToString("yyyy"),
month = post.Published.ToString("MM"),
day = post.Published.ToString("dd"),
slug = post.Slug
})
}));
}
[HttpGet("author/{id:guid}")]
public IActionResult GetAuthor(Guid id)
{
if (!ValidateReferer()) return NotFound();
if (!_userService.TryGetUser(id, out IUser? author)) return NotFound();
return Ok(new
{
id = author.Id,
name = author.DisplayName,
avatarUrl = author.AvatarUrl,
});
}
private bool ValidateReferer()
{
var referer = Request.Headers["Referer"].ToString();
return referer.StartsWith(Url.PageLink("/index")!);
}
}

View File

@ -8,7 +8,7 @@ internal sealed class UserConfiguration : IEntityTypeConfiguration<User>
/// <inheritdoc />
public void Configure(EntityTypeBuilder<User> builder)
{
RelationalEntityTypeBuilderExtensions.ToTable((EntityTypeBuilder)builder, "User");
builder.ToTable("User");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).IsRequired();

View File

@ -5,7 +5,6 @@ using OliverBooth.Common;
using OliverBooth.Common.Extensions;
using OliverBooth.Common.Services;
using Serilog;
using X10D.Hosting.DependencyInjection;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
@ -17,6 +16,7 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true);
builder.Logging.ClearProviders();
builder.Logging.AddSerilog();
builder.Services.AddMarkdownPipeline();
builder.Services.ConfigureOptions<OliverBoothConfigureOptions>();
builder.Services.AddDbContextFactory<BlogContext>();
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();

View File

@ -0,0 +1,28 @@
using Markdig;
using Microsoft.Extensions.DependencyInjection;
using OliverBooth.Common.Markdown;
using OliverBooth.Common.Services;
namespace OliverBooth.Common.Extensions;
/// <summary>
/// Extension methods for <see cref="IServiceCollection" />.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds the Markdown pipeline to the <see cref="IServiceCollection" />.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection" />.</param>
/// <returns>The <see cref="IServiceCollection" />.</returns>
public static IServiceCollection AddMarkdownPipeline(this IServiceCollection serviceCollection)
{
return serviceCollection.AddSingleton(provider => new MarkdownPipelineBuilder()
.Use(new TemplateExtension(provider.GetRequiredService<ITemplateService>()))
.UseAdvancedExtensions()
.UseBootstrap()
.UseEmojiAndSmiley()
.UseSmartyPants()
.Build());
}
}

View File

@ -1,10 +1,7 @@
using Markdig;
using OliverBooth.Common;
using OliverBooth.Common.Extensions;
using OliverBooth.Common.Markdown;
using OliverBooth.Common.Services;
using OliverBooth.Data;
using OliverBooth.Markdown.Timestamp;
using OliverBooth.Services;
using Serilog;
@ -18,18 +15,9 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true);
builder.Logging.ClearProviders();
builder.Logging.AddSerilog();
builder.Services.AddMarkdownPipeline();
builder.Services.ConfigureOptions<OliverBoothConfigureOptions>();
builder.Services.AddSingleton<ITemplateService, TemplateService>();
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
.Use<TimestampExtension>()
.Use(new TemplateExtension(provider.GetRequiredService<ITemplateService>()))
.UseAdvancedExtensions()
.UseBootstrap()
.UseEmojiAndSmiley()
.UseSmartyPants()
.Build());
builder.Services.AddDbContextFactory<WebContext>();
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
builder.Services.AddControllersWithViews();

View File

@ -2,25 +2,37 @@ import BlogPost from "./BlogPost";
import Author from "./Author";
class API {
private static readonly BASE_URL: string = "/api";
private static readonly BASE_URL: string = "https://api.oliverbooth.dev";
private static readonly BLOG_URL: string = "/blog";
static async getBlogPostCount(): Promise<number> {
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/count`);
const text = await response.text();
return JSON.parse(text).count;
const response = await API.getResponse(`count`);
return response.count;
}
static async getBlogPosts(skip: number, take: number): Promise<BlogPost[]> {
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/all/${skip}/${take}`);
const text = await response.text();
return JSON.parse(text).map(obj => new BlogPost(obj));
static async getBlogPost(id: string): Promise<BlogPost> {
const response = await API.getResponse(`post/${id}`);
return new BlogPost(response);
}
static async getBlogPosts(page: number): Promise<BlogPost[]> {
const response = await API.getResponse(`posts/${page}`);
return response.map(obj => new BlogPost(obj));
}
static async getAuthor(id: string): Promise<Author> {
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/author/${id}`);
const response = await API.getResponse(`author/${id}`);
return new Author(response);
}
private static async getResponse(url: string): Promise<any> {
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/${url}`);
if (response.status !== 200) {
throw new Error("Invalid response from server");
}
const text = await response.text();
return new Author(JSON.parse(text));
return JSON.parse(text);
}
}

View File

@ -1,12 +1,12 @@
class Author {
private readonly _id: string;
private readonly _name: string;
private readonly _avatarHash: string;
private readonly _avatarUrl: string;
constructor(json: any) {
this._id = json.id;
this._name = json.name;
this._avatarHash = json.avatarHash;
this._avatarUrl = json.avatarUrl;
}
get id(): string {
@ -17,8 +17,8 @@ class Author {
return this._name;
}
get avatarHash(): string {
return this._avatarHash;
get avatarUrl(): string {
return this._avatarUrl;
}
}

View File

@ -3,6 +3,7 @@ class BlogPost {
private readonly _commentsEnabled: boolean;
private readonly _title: string;
private readonly _excerpt: string;
private readonly _content: string;
private readonly _authorId: string;
private readonly _published: Date;
private readonly _updated?: Date;
@ -17,6 +18,7 @@ class BlogPost {
this._commentsEnabled = json.commentsEnabled;
this._title = json.title;
this._excerpt = json.excerpt;
this._content = json.content;
this._authorId = json.author;
this._published = new Date(json.published * 1000);
this._updated = (json.updated && new Date(json.updated * 1000)) || null;
@ -43,6 +45,10 @@ class BlogPost {
return this._excerpt;
}
get content(): string {
return this._content;
}
get authorId(): string {
return this._authorId;
}

View File

@ -54,7 +54,7 @@ class UI {
},
author: {
name: author.name,
avatar: `https://gravatar.com/avatar/${author.avatarHash}?s=28`,
avatar: author.avatarUrl
}
});
card.innerHTML = body.trim();

View File

@ -39,8 +39,8 @@ declare const Prism: any;
const authors = [];
const template = Handlebars.compile(UI.blogPostTemplate.innerHTML);
API.getBlogPostCount().then(async (count) => {
for (let i = 0; i < count; i += 5) {
const posts = await API.getBlogPosts(i, 5);
for (let i = 0; i <= count / 10; i++) {
const posts = await API.getBlogPosts(i);
for (const post of posts) {
let author: Author;
if (authors[post.authorId]) {