catcrafts.net/implementations/Catcrafts-Views.cpp

104 lines
4 KiB
C++
Raw Permalink Normal View History

/*
catcrafts.net
Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only.
No permission is granted to copy, modify, distribute, or create derivative works.
*/
export module Catcrafts:Views_impl;
import :Views;
import Crafter.Graphics;
2026-08-05 04:18:37 +02:00
import Catcrafts.Shared;
import std;
using namespace Crafter;
namespace Catcrafts {
namespace {
2026-08-05 04:18:37 +02:00
// Owning handle for the page chrome, ONLY when this module created it.
// std::optional defers construction until InitializePage() runs — a
// CreateInBody at static-init time would fire before main().
//
// Left empty when the server already rendered the chrome: the handle is
// owning, and destroying it would remove the server's markup from the
// document.
std::optional<Dom::HtmlElement> root;
2026-08-05 04:18:37 +02:00
// True when the document arrived server-rendered.
bool adoptedSsr = false;
Views::SiteContent content;
bool contentLoaded = false;
Window* activeWindow = nullptr;
}
void SetActiveWindow(Window* window) { activeWindow = window; }
void SetDocumentTitle(std::string_view title) {
// Only meaningful on the client-rendered path. On an SSR'd page the
// server already set a correct <title>, and overwriting it with the
// same string is pointless work.
if (activeWindow && !adoptedSsr) activeWindow->SetTitle(title);
}
bool AdoptedSsr() { return adoptedSsr; }
std::string ReadBundleFile(std::string_view name) {
// cfg.files flattens to the bundle root (Crafter.Build copies by
// filename, not by path), so "content/posts.json" is readable as
// "posts.json" here. runtime.js has already fetched every files.json
// entry into the VFS before _start, so this cannot block on the
// network and cannot fail for a file that is in the manifest.
std::ifstream in(std::string(name), std::ios::binary);
if (!in) return {};
std::ostringstream buf;
buf << in.rdbuf();
return buf.str();
}
const Views::SiteContent& SiteData() {
if (!contentLoaded) {
contentLoaded = true;
// Authored content is compiled into the module; only the
// pipeline-generated files ride the VFS.
content.projects = Content::Projects();
content.products = Content::Products();
content.legal = Content::LegalPages();
content.demos = Content::Demos();
content.posts = LoadPosts(ReadBundleFile("posts.json"));
content.rates = LoadRates(ReadBundleFile("rates.json"));
}
return content;
}
void InitializePage() {
2026-08-05 04:18:37 +02:00
// Two ways a page can arrive, and they need opposite handling.
//
// 1. Server-rendered (the normal case): #catcrafts-root already exists,
// with the chrome and the route's content in it. Building a second
// root here would duplicate the header and footer on screen, and
// re-rendering <main> would replace correct markup with identical
// markup — a wasted round of DOM work and a visible flash for
// nothing. So: adopt, and touch nothing.
//
// 2. The static fallback shell: Caddy serves the wasm bundle's
// index.html when the backend is down (see deploy/Caddyfile.example),
// and that document has an empty <body>. Here the app IS the whole
// site and has to build the chrome and render the route itself.
Dom::HtmlElementPtr existing("catcrafts-root");
if (existing.ptr != 0) {
adoptedSsr = true;
return;
}
root.emplace(Dom::HtmlElement::CreateInBody("div", "catcrafts-root"));
2026-08-05 04:18:37 +02:00
root->SetInnerHTML(std::format(
R"(<header id="cc-header">{}</header>)"
R"(<main id="main"></main>)"
R"(<footer id="cc-footer">{}</footer>)",
Views::RenderNav(RouteKind::Home).Str(),
Views::RenderFooter().Str()));
}
}