2025-11-12 20:58:07 +01:00
|
|
|
/*
|
|
|
|
|
catcrafts.net
|
2026-07-18 23:27:41 +02:00
|
|
|
Copyright (C) 2026 Catcrafts
|
2025-11-12 20:58:07 +01:00
|
|
|
|
2026-07-18 23:27:41 +02:00
|
|
|
The source code of this website is made available for viewing purposes only.
|
2025-11-12 20:58:07 +01:00
|
|
|
No permission is granted to copy, modify, distribute, or create derivative works.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export module Catcrafts:Blog_impl;
|
|
|
|
|
import :Blog;
|
2025-11-12 21:56:18 +01:00
|
|
|
import :Root;
|
|
|
|
|
import :Views;
|
2026-07-19 01:13:30 +02:00
|
|
|
import :Demo;
|
2026-07-18 23:27:41 +02:00
|
|
|
import Crafter.Graphics;
|
2025-11-12 20:58:07 +01:00
|
|
|
import std;
|
|
|
|
|
|
2026-07-18 23:27:41 +02:00
|
|
|
using namespace Crafter;
|
2025-11-12 21:56:18 +01:00
|
|
|
|
2026-07-19 01:13:30 +02:00
|
|
|
namespace Catcrafts {
|
|
|
|
|
// Post that hosts the live ray-traced WebGPU demo. RenderBlogPost
|
|
|
|
|
// injects the mount container into this post and mounts the render
|
|
|
|
|
// canvas into it once the DOM is in place.
|
|
|
|
|
constexpr std::string_view kDemoPostSlug = "hello-world-2";
|
|
|
|
|
|
|
|
|
|
// Markup for the embedded demo: a fixed-height host box the render
|
|
|
|
|
// canvas is reparented into (see Catcrafts:Demo / WebGPU::SetCanvasMount)
|
|
|
|
|
// plus a caption. The id must match kDemoMountId.
|
|
|
|
|
constexpr std::string_view kDemoCardHtml = R"(
|
|
|
|
|
<div class="webgpu-demo">
|
|
|
|
|
<div id="webgpu-demo" class="webgpu-demo-canvas"></div>
|
|
|
|
|
<p class="webgpu-demo-caption">
|
|
|
|
|
Live above: a scene ray-traced in real time — four coloured
|
|
|
|
|
point lights, one soft shadow per light — running through the
|
|
|
|
|
Crafter.Graphics WebGPU wavefront tracer, driven from this page's
|
|
|
|
|
C++ compiled to WebAssembly. Not a video, not an iframe: the same
|
|
|
|
|
WASM module that rendered this text is tracing those pixels.
|
|
|
|
|
</p>
|
|
|
|
|
</div>)";
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 21:56:18 +01:00
|
|
|
export namespace Catcrafts {
|
2026-07-18 23:27:41 +02:00
|
|
|
// Persistent storage for the per-post card click handlers. HtmlElementPtr
|
|
|
|
|
// unregisters its listeners on destruction, so the elements have to
|
|
|
|
|
// outlive Render(); clear() at the top of RenderBlog() detaches the
|
|
|
|
|
// previous render's handlers before we attach the new ones.
|
|
|
|
|
std::vector<Dom::HtmlElementPtr> blogButtons;
|
2025-11-12 21:56:18 +01:00
|
|
|
|
|
|
|
|
void RenderBlog() {
|
2025-11-14 23:56:39 +01:00
|
|
|
blogButtons.clear();
|
2025-11-12 20:58:07 +01:00
|
|
|
std::string html = "";
|
2025-11-14 23:56:39 +01:00
|
|
|
for(const BlogPost& post : posts) {
|
2025-11-12 21:56:18 +01:00
|
|
|
std::string previewContent = post.content;
|
|
|
|
|
if(previewContent.length() > 200) {
|
|
|
|
|
std::size_t lastSpace = previewContent.find_last_of(' ', 200);
|
|
|
|
|
if(lastSpace != std::string::npos) {
|
|
|
|
|
previewContent = previewContent.substr(0, lastSpace) + "...";
|
|
|
|
|
} else {
|
|
|
|
|
previewContent = previewContent.substr(0, 200) + "...";
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
html += std::format(R"(
|
2025-11-12 22:11:56 +01:00
|
|
|
<div class="post fade-in" id="blog-post-{}">
|
2025-11-12 21:24:23 +01:00
|
|
|
<div class="post-header">
|
2025-11-12 21:56:18 +01:00
|
|
|
<h2 class="post-title"><a>{}</a></h2>
|
2025-11-12 21:24:23 +01:00
|
|
|
<span class="post-date">{}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="post-content">
|
|
|
|
|
{}
|
|
|
|
|
</div>
|
2025-11-12 21:56:18 +01:00
|
|
|
<div class="post-footer">
|
2025-11-12 22:11:56 +01:00
|
|
|
<a class="btn">Read Full Post</a>
|
2025-11-12 21:56:18 +01:00
|
|
|
</div>
|
2025-11-12 22:11:56 +01:00
|
|
|
</div>)", post.slug, post.name, post.date, previewContent);
|
2025-11-12 20:58:07 +01:00
|
|
|
}
|
2026-07-18 23:27:41 +02:00
|
|
|
MainContent().SetInnerHTML(std::format(R"(<div class="blog-posts">{}</div>)", html));
|
2025-11-12 21:56:18 +01:00
|
|
|
|
2025-11-14 23:56:39 +01:00
|
|
|
for(const BlogPost& post : posts) {
|
2026-07-18 23:27:41 +02:00
|
|
|
Dom::HtmlElementPtr& cardView = blogButtons.emplace_back(std::format("blog-post-{}", post.slug));
|
|
|
|
|
cardView.AddClickListener([slug = post.slug](Dom::MouseEvent) {
|
|
|
|
|
Router::PushState("{}", "", std::format("/blog/{}", slug));
|
2025-11-12 21:56:18 +01:00
|
|
|
RenderRoot(std::format("/blog/{}", slug));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-18 23:27:41 +02:00
|
|
|
|
2025-11-12 21:56:18 +01:00
|
|
|
void RenderBlogPost(const std::string_view slug) {
|
2025-11-14 23:56:39 +01:00
|
|
|
for(const BlogPost& post : posts) {
|
2025-11-12 21:56:18 +01:00
|
|
|
if(post.slug == slug) {
|
2026-07-19 01:13:30 +02:00
|
|
|
const bool isDemo = post.slug == kDemoPostSlug;
|
2026-07-18 23:27:41 +02:00
|
|
|
MainContent().SetInnerHTML(std::format(R"(
|
2025-11-12 22:40:33 +01:00
|
|
|
<div class="blog-post-page">
|
2025-11-12 21:56:18 +01:00
|
|
|
<div class="post-header">
|
|
|
|
|
<h1 class="post-title">{}</h1>
|
|
|
|
|
<span class="post-date">{}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="post-content">
|
|
|
|
|
{}
|
|
|
|
|
</div>
|
2026-07-19 01:13:30 +02:00
|
|
|
{}
|
|
|
|
|
</div>)", post.name, post.date, post.content, isDemo ? kDemoCardHtml : std::string_view{}));
|
|
|
|
|
|
|
|
|
|
// The #webgpu-demo container now exists in the DOM, so the
|
|
|
|
|
// render canvas can be reparented into it and tracing can
|
|
|
|
|
// start. RenderRoot() already called UnmountDemo() for us.
|
|
|
|
|
if(isDemo) MountDemo();
|
2025-11-12 21:56:18 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 23:27:41 +02:00
|
|
|
MainContent().SetInnerHTML("<h1>Post Not Found</h1><p>The requested blog post could not be found.</p>");
|
2025-11-12 20:58:07 +01:00
|
|
|
}
|
2026-07-18 23:27:41 +02:00
|
|
|
}
|