Implemented featured endpoint and embed script
This commit is contained in:
parent
1e10d41cad
commit
97a73f0a32
38
Wave/Controllers/ApiController.cs
Normal file
38
Wave/Controllers/ApiController.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Wave.Data;
|
||||
using Wave.Data.Api;
|
||||
|
||||
namespace Wave.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("/[controller]")]
|
||||
public class ApiController(ApplicationDbContext context, IOptions<Customization> customizationOptions) : ControllerBase {
|
||||
|
||||
[HttpGet("article/featured")]
|
||||
[Produces("application/json")]
|
||||
public async Task<Results<Ok<ArticleDto>, NoContent>> GetArticleFeatured([FromQuery, Range(16, 800)] int profilePictureSize = 800) {
|
||||
Response.Headers.AccessControlAllowOrigin = "*";
|
||||
|
||||
var article = await context.Set<Article>()
|
||||
.IgnoreAutoIncludes()
|
||||
.Include(a => a.Author).ThenInclude(a => a.Articles)
|
||||
.Include(a => a.Reviewer)
|
||||
.Include(a => a.Categories)
|
||||
.OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (article is null) return TypedResults.NoContent();
|
||||
|
||||
return TypedResults.Ok(ArticleDto.GetFromArticle(article, GetHost(), profilePictureSize));
|
||||
}
|
||||
|
||||
private Uri GetHost() {
|
||||
string customUrl = customizationOptions.Value.AppUrl;
|
||||
|
||||
if (!string.IsNullOrEmpty(customUrl)) return new Uri(customUrl, UriKind.Absolute);
|
||||
return new Uri($"{Request.Scheme}://{Request.Host}");
|
||||
}
|
||||
}
|
28
Wave/Data/Api/ArticleDto.cs
Normal file
28
Wave/Data/Api/ArticleDto.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using Wave.Utilities;
|
||||
|
||||
namespace Wave.Data.Api;
|
||||
|
||||
public record ArticleDto(
|
||||
string Title,
|
||||
string ContentPreview,
|
||||
string BrowserUrl,
|
||||
DateTimeOffset PublishDate,
|
||||
UserDto Author,
|
||||
UserDto? Reviewer,
|
||||
IList<CategoryDto> Categories) {
|
||||
|
||||
public static ArticleDto GetFromArticle(Article article, Uri host, int pfpSize) {
|
||||
string browserLink = ArticleUtilities.GenerateArticleLink(article, host);
|
||||
|
||||
var author = UserDto.GetFromUser(article.Author, host, pfpSize);
|
||||
var reviewer =
|
||||
article.Reviewer is not null &&
|
||||
article.Reviewer.Id != article.Author.Id
|
||||
? UserDto.GetFromUser(article.Reviewer, host, pfpSize) : null;
|
||||
|
||||
var categories = article.Categories.Select(c => new CategoryDto(c)).ToArray();
|
||||
string preview = article.BodyPlain[..Math.Min(article.BodyPlain.Length, 500)];
|
||||
|
||||
return new ArticleDto(article.Title, preview, browserLink, article.PublishDate, author, reviewer, categories);
|
||||
}
|
||||
}
|
9
Wave/Data/Api/CategoryDto.cs
Normal file
9
Wave/Data/Api/CategoryDto.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using Wave.Utilities;
|
||||
|
||||
namespace Wave.Data.Api;
|
||||
|
||||
public record CategoryDto(string Name, string Role) {
|
||||
|
||||
public CategoryDto(Category category)
|
||||
: this(category.Name, CategoryUtilities.GetCssClassPostfixForColor(category.Color)) {}
|
||||
}
|
14
Wave/Data/Api/UserDto.cs
Normal file
14
Wave/Data/Api/UserDto.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace Wave.Data.Api;
|
||||
|
||||
public record UserDto(
|
||||
string Name,
|
||||
string ProfilePictureUrl,
|
||||
string? ProfileUrl) {
|
||||
|
||||
public static UserDto GetFromUser(ApplicationUser user, Uri host, int pfpSize) {
|
||||
var pfpUrl = new Uri(host, $"/api/User/pfp/{user.Id}?size={pfpSize}");
|
||||
var profileUrl = user.Articles.Count > 0 ? new Uri(host, $"/profile/{user.Id}") : null;
|
||||
|
||||
return new UserDto(user.FullName ?? "Guest Author", pfpUrl.AbsoluteUri, profileUrl?.AbsoluteUri);
|
||||
}
|
||||
}
|
86
Wave/wwwroot/featured.js
Normal file
86
Wave/wwwroot/featured.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
const doInsertText = function(dataName, content) {
|
||||
const element = document.querySelector(`[data-wave-${dataName}]`);
|
||||
if (element) {
|
||||
if (element.tagName === "A") {
|
||||
element.href = content;
|
||||
} else if (element.tagName === "IMG") {
|
||||
element.src = content;
|
||||
} else {
|
||||
element.innerText = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const script = document.getElementById("wave-script");
|
||||
if (!script) {
|
||||
throw new Error("[WAVE] no script with the id 'wave-script' exists.");
|
||||
}
|
||||
if (!script.src) {
|
||||
throw new Error("[WAVE] failed to get src attribute of element with id 'wave-script'.");
|
||||
}
|
||||
const scriptUrl = new URL(script.src);
|
||||
const host = `${scriptUrl.protocol}//${scriptUrl.host}`;
|
||||
|
||||
let pfpSize = 150;
|
||||
const container = document.querySelector("[data-wave]");
|
||||
|
||||
if (container && container.dataset.wavePfpSize) {
|
||||
const value = parseInt(container.dataset.wavePfpSize);
|
||||
if (value && value > 800) console.log("[WAVE] WARNING: pfp sizes greater 800 are not supported.");
|
||||
else if (value) pfpSize = value;
|
||||
else console.log(
|
||||
"[WAVE] WARNING: a custom pfp size has been provided with 'data-wave-pfp-size', " +
|
||||
"but it's value could not be parsed as an integer.");
|
||||
}
|
||||
|
||||
console.log("[WAVE] requesting featured article");
|
||||
fetch(new URL("/api/article/featured?size=" + pfpSize, host),
|
||||
{
|
||||
method: "GET",
|
||||
headers: new Headers({
|
||||
"Accept": "application/json"
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(function(result) {
|
||||
|
||||
if (container) {
|
||||
const template = document.querySelector("[data-wave-template]");
|
||||
|
||||
if (template) {
|
||||
container.innerHTML = "";
|
||||
container.appendChild(template.content.cloneNode(true));
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div style="padding: 1em; border: 1px solid black; box-shadow: 4px 4px 0 0 currentColor; background: #ffb3c8;">
|
||||
<h1 data-wave-title style="margin: 0 0 0.5em 0"></h1>
|
||||
<img style="float: left; margin: 0 0.5em 0.5em 0; border: 1px solid transparent; border-radius: 0.25em"
|
||||
data-wave-author-profilePictureUrl alt="" width="${pfpSize}" />
|
||||
<p style="line-height: 1.4em; margin: 0">
|
||||
<a data-wave-author-profileUrl target="_blank" style="text-decoration: none; color: black">
|
||||
<small data-wave-author-name style="font-weight: bold"></small><br>
|
||||
</a>
|
||||
<small data-wave-publishDate></small><br>
|
||||
<span data-wave-contentPreview></span><br>
|
||||
<a data-wave-browserUrl target="_blank" style="color: black">Read More</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
for (let [key, value] of Object.entries(result)) {
|
||||
doInsertText(key, value);
|
||||
if (typeof value === "object" && value != null) {
|
||||
for (let [innerKey, innerValue] of Object.entries(value)) {
|
||||
doInsertText(key + "-" + innerKey, innerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[WAVE] fetched feature successfully.");
|
||||
} else {
|
||||
console.log("[WAVE] no container found, to use featured you require an element with the data tag 'wave'.");
|
||||
}
|
||||
})
|
||||
.catch(err => console.log(`[WAVE] failed to request featured article: ${err}.`));
|
Loading…
Reference in a new issue