Compare commits
No commits in common. "94752051969ca5a41976d1ca920be079657f0248" and "7495da56cbac70001152bc9b469c0f64eca95c17" have entirely different histories.
9475205196
...
7495da56cb
96
OliverBooth.Blog/Controllers/BlogApiController.cs
Normal file
96
OliverBooth.Blog/Controllers/BlogApiController.cs
Normal file
@ -0,0 +1,96 @@
|
||||
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 />
|
||||
public void Configure(EntityTypeBuilder<User> builder)
|
||||
{
|
||||
builder.ToTable("User");
|
||||
RelationalEntityTypeBuilderExtensions.ToTable((EntityTypeBuilder)builder, "User");
|
||||
builder.HasKey(e => e.Id);
|
||||
|
||||
builder.Property(e => e.Id).IsRequired();
|
||||
|
@ -5,6 +5,7 @@ using OliverBooth.Common;
|
||||
using OliverBooth.Common.Extensions;
|
||||
using OliverBooth.Common.Services;
|
||||
using Serilog;
|
||||
using X10D.Hosting.DependencyInjection;
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Console()
|
||||
@ -16,7 +17,6 @@ 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>();
|
||||
|
@ -1,28 +0,0 @@
|
||||
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,7 +1,10 @@
|
||||
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;
|
||||
|
||||
@ -15,9 +18,18 @@ 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();
|
||||
|
@ -2,37 +2,25 @@ import BlogPost from "./BlogPost";
|
||||
import Author from "./Author";
|
||||
|
||||
class API {
|
||||
private static readonly BASE_URL: string = "https://api.oliverbooth.dev";
|
||||
private static readonly BASE_URL: string = "/api";
|
||||
private static readonly BLOG_URL: string = "/blog";
|
||||
|
||||
static async getBlogPostCount(): Promise<number> {
|
||||
const response = await API.getResponse(`count`);
|
||||
return response.count;
|
||||
}
|
||||
|
||||
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 API.getResponse(`author/${id}`);
|
||||
return new Author(response);
|
||||
const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/count`);
|
||||
const text = await response.text();
|
||||
return JSON.parse(text).count;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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);
|
||||
return JSON.parse(text).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 text = await response.text();
|
||||
return new Author(JSON.parse(text));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
class Author {
|
||||
private readonly _id: string;
|
||||
private readonly _name: string;
|
||||
private readonly _avatarUrl: string;
|
||||
private readonly _avatarHash: string;
|
||||
|
||||
constructor(json: any) {
|
||||
this._id = json.id;
|
||||
this._name = json.name;
|
||||
this._avatarUrl = json.avatarUrl;
|
||||
this._avatarHash = json.avatarHash;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
@ -17,8 +17,8 @@ class Author {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
get avatarUrl(): string {
|
||||
return this._avatarUrl;
|
||||
get avatarHash(): string {
|
||||
return this._avatarHash;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ 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;
|
||||
@ -18,7 +17,6 @@ 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;
|
||||
@ -45,10 +43,6 @@ class BlogPost {
|
||||
return this._excerpt;
|
||||
}
|
||||
|
||||
get content(): string {
|
||||
return this._content;
|
||||
}
|
||||
|
||||
get authorId(): string {
|
||||
return this._authorId;
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ class UI {
|
||||
},
|
||||
author: {
|
||||
name: author.name,
|
||||
avatar: author.avatarUrl
|
||||
avatar: `https://gravatar.com/avatar/${author.avatarHash}?s=28`,
|
||||
}
|
||||
});
|
||||
card.innerHTML = body.trim();
|
||||
|
@ -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 / 10; i++) {
|
||||
const posts = await API.getBlogPosts(i);
|
||||
for (let i = 0; i < count; i += 5) {
|
||||
const posts = await API.getBlogPosts(i, 5);
|
||||
for (const post of posts) {
|
||||
let author: Author;
|
||||
if (authors[post.authorId]) {
|
||||
|
Loading…
Reference in New Issue
Block a user