Compare commits
3 Commits
7495da56cb
...
9475205196
Author | SHA1 | Date | |
---|---|---|---|
9475205196 | |||
f878bff8f3 | |||
a84f537dc1 |
@ -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")!);
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,7 @@ internal sealed class UserConfiguration : IEntityTypeConfiguration<User>
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(EntityTypeBuilder<User> builder)
|
public void Configure(EntityTypeBuilder<User> builder)
|
||||||
{
|
{
|
||||||
RelationalEntityTypeBuilderExtensions.ToTable((EntityTypeBuilder)builder, "User");
|
builder.ToTable("User");
|
||||||
builder.HasKey(e => e.Id);
|
builder.HasKey(e => e.Id);
|
||||||
|
|
||||||
builder.Property(e => e.Id).IsRequired();
|
builder.Property(e => e.Id).IsRequired();
|
||||||
|
@ -5,7 +5,6 @@ using OliverBooth.Common;
|
|||||||
using OliverBooth.Common.Extensions;
|
using OliverBooth.Common.Extensions;
|
||||||
using OliverBooth.Common.Services;
|
using OliverBooth.Common.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using X10D.Hosting.DependencyInjection;
|
|
||||||
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.WriteTo.Console()
|
.WriteTo.Console()
|
||||||
@ -17,6 +16,7 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true);
|
|||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddSerilog();
|
builder.Logging.AddSerilog();
|
||||||
|
|
||||||
|
builder.Services.AddMarkdownPipeline();
|
||||||
builder.Services.ConfigureOptions<OliverBoothConfigureOptions>();
|
builder.Services.ConfigureOptions<OliverBoothConfigureOptions>();
|
||||||
builder.Services.AddDbContextFactory<BlogContext>();
|
builder.Services.AddDbContextFactory<BlogContext>();
|
||||||
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
||||||
|
28
OliverBooth.Common/Extensions/ServiceCollectionExtensions.cs
Normal file
28
OliverBooth.Common/Extensions/ServiceCollectionExtensions.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,7 @@
|
|||||||
using Markdig;
|
|
||||||
using OliverBooth.Common;
|
using OliverBooth.Common;
|
||||||
using OliverBooth.Common.Extensions;
|
using OliverBooth.Common.Extensions;
|
||||||
using OliverBooth.Common.Markdown;
|
|
||||||
using OliverBooth.Common.Services;
|
using OliverBooth.Common.Services;
|
||||||
using OliverBooth.Data;
|
using OliverBooth.Data;
|
||||||
using OliverBooth.Markdown.Timestamp;
|
|
||||||
using OliverBooth.Services;
|
using OliverBooth.Services;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@ -18,18 +15,9 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true);
|
|||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddSerilog();
|
builder.Logging.AddSerilog();
|
||||||
|
|
||||||
|
builder.Services.AddMarkdownPipeline();
|
||||||
builder.Services.ConfigureOptions<OliverBoothConfigureOptions>();
|
builder.Services.ConfigureOptions<OliverBoothConfigureOptions>();
|
||||||
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
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.AddDbContextFactory<WebContext>();
|
||||||
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
|
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
|
@ -2,25 +2,37 @@ import BlogPost from "./BlogPost";
|
|||||||
import Author from "./Author";
|
import Author from "./Author";
|
||||||
|
|
||||||
class API {
|
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";
|
private static readonly BLOG_URL: string = "/blog";
|
||||||
|
|
||||||
static async getBlogPostCount(): Promise<number> {
|
static async getBlogPostCount(): Promise<number> {
|
||||||
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/count`);
|
const response = await API.getResponse(`count`);
|
||||||
const text = await response.text();
|
return response.count;
|
||||||
return JSON.parse(text).count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getBlogPosts(skip: number, take: number): Promise<BlogPost[]> {
|
static async getBlogPost(id: string): Promise<BlogPost> {
|
||||||
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/all/${skip}/${take}`);
|
const response = await API.getResponse(`post/${id}`);
|
||||||
const text = await response.text();
|
return new BlogPost(response);
|
||||||
return JSON.parse(text).map(obj => new BlogPost(obj));
|
}
|
||||||
|
|
||||||
|
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> {
|
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();
|
const text = await response.text();
|
||||||
return new Author(JSON.parse(text));
|
return JSON.parse(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
class Author {
|
class Author {
|
||||||
private readonly _id: string;
|
private readonly _id: string;
|
||||||
private readonly _name: string;
|
private readonly _name: string;
|
||||||
private readonly _avatarHash: string;
|
private readonly _avatarUrl: string;
|
||||||
|
|
||||||
constructor(json: any) {
|
constructor(json: any) {
|
||||||
this._id = json.id;
|
this._id = json.id;
|
||||||
this._name = json.name;
|
this._name = json.name;
|
||||||
this._avatarHash = json.avatarHash;
|
this._avatarUrl = json.avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id(): string {
|
get id(): string {
|
||||||
@ -17,8 +17,8 @@ class Author {
|
|||||||
return this._name;
|
return this._name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get avatarHash(): string {
|
get avatarUrl(): string {
|
||||||
return this._avatarHash;
|
return this._avatarUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ class BlogPost {
|
|||||||
private readonly _commentsEnabled: boolean;
|
private readonly _commentsEnabled: boolean;
|
||||||
private readonly _title: string;
|
private readonly _title: string;
|
||||||
private readonly _excerpt: string;
|
private readonly _excerpt: string;
|
||||||
|
private readonly _content: string;
|
||||||
private readonly _authorId: string;
|
private readonly _authorId: string;
|
||||||
private readonly _published: Date;
|
private readonly _published: Date;
|
||||||
private readonly _updated?: Date;
|
private readonly _updated?: Date;
|
||||||
@ -17,6 +18,7 @@ class BlogPost {
|
|||||||
this._commentsEnabled = json.commentsEnabled;
|
this._commentsEnabled = json.commentsEnabled;
|
||||||
this._title = json.title;
|
this._title = json.title;
|
||||||
this._excerpt = json.excerpt;
|
this._excerpt = json.excerpt;
|
||||||
|
this._content = json.content;
|
||||||
this._authorId = json.author;
|
this._authorId = json.author;
|
||||||
this._published = new Date(json.published * 1000);
|
this._published = new Date(json.published * 1000);
|
||||||
this._updated = (json.updated && new Date(json.updated * 1000)) || null;
|
this._updated = (json.updated && new Date(json.updated * 1000)) || null;
|
||||||
@ -43,6 +45,10 @@ class BlogPost {
|
|||||||
return this._excerpt;
|
return this._excerpt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get content(): string {
|
||||||
|
return this._content;
|
||||||
|
}
|
||||||
|
|
||||||
get authorId(): string {
|
get authorId(): string {
|
||||||
return this._authorId;
|
return this._authorId;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ class UI {
|
|||||||
},
|
},
|
||||||
author: {
|
author: {
|
||||||
name: author.name,
|
name: author.name,
|
||||||
avatar: `https://gravatar.com/avatar/${author.avatarHash}?s=28`,
|
avatar: author.avatarUrl
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
card.innerHTML = body.trim();
|
card.innerHTML = body.trim();
|
||||||
|
@ -39,8 +39,8 @@ declare const Prism: any;
|
|||||||
const authors = [];
|
const authors = [];
|
||||||
const template = Handlebars.compile(UI.blogPostTemplate.innerHTML);
|
const template = Handlebars.compile(UI.blogPostTemplate.innerHTML);
|
||||||
API.getBlogPostCount().then(async (count) => {
|
API.getBlogPostCount().then(async (count) => {
|
||||||
for (let i = 0; i < count; i += 5) {
|
for (let i = 0; i <= count / 10; i++) {
|
||||||
const posts = await API.getBlogPosts(i, 5);
|
const posts = await API.getBlogPosts(i);
|
||||||
for (const post of posts) {
|
for (const post of posts) {
|
||||||
let author: Author;
|
let author: Author;
|
||||||
if (authors[post.authorId]) {
|
if (authors[post.authorId]) {
|
||||||
|
Loading…
Reference in New Issue
Block a user