mabel-hex/src/inbound/renderer/mod.rs
2025-02-06 21:11:43 +01:00

342 lines
8.7 KiB
Rust

#![allow(non_snake_case)]
use dioxus::prelude::*;
use meta::SiteType;
use page::{Page, PostElement};
use crate::domain::entities::site::PageContent;
mod admin_ui;
mod meta;
mod page;
mod server;
#[cfg(test)]
mod testing;
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[rustfmt::skip]
enum Route {
#[layout(SiteLayout)]
#[route("/")]
Home {},
#[nest("/p")]
#[route("/:page")]
Single { page: String },
#[route("/:page/:id")]
Post { page: String, id: String },
#[end_nest]
#[route("/:..route")]
PageNotFound { route: Vec<String> },
}
pub fn App() -> Element {
// Check for special redirects
let site_res = use_server_future(server::get_site_info)?.suspend()?;
let site_type = site_res.read().clone();
match site_type {
Ok(SiteType::ControlPanel) => {
rsx! {
admin_ui::App {}
}
}
Ok(SiteType::UserSite(site_info)) => {
// Inject site info in context
meta::set_site(&site_info);
rsx! {
Router::<Route> {}
}
}
_ => {
rsx! {
h1 { "404" }
}
}
}
}
#[component]
fn PageNotFound(route: Vec<String>) -> Element {
rsx! {
h1 { "404 - {route:?}" }
}
}
#[component]
fn PageLink(page: String, title: String, current: Route) -> Element {
let to = if page == "/" {
Route::Home {}
} else {
Route::Single { page }
};
let class = if current == to { "active" } else { "" };
rsx! {
Link { to, class, "{title}" }
}
}
#[component]
fn SiteLayout() -> Element {
let site = meta::site();
let route = use_route::<Route>();
rsx! {
header { class: "site-header",
nav { class: "page-list",
ul {
for page in site.info.pages {
li {
PageLink {
page: page.name,
title: page.title,
current: route.clone(),
}
}
}
}
}
h1 { "{site.info.title}" }
}
SuspenseBoundary { fallback: |_context: SuspenseContext| rsx! { "..." },
main { Outlet::<Route> {} }
}
}
}
#[component]
fn Home() -> Element {
let site = meta::site();
let page_ref =
use_server_future(move || server::get_page(site.info.domain.clone(), "/".to_string()))?
.suspend()?;
match &*page_ref.read() {
Ok(page) => meta::set_page(page.clone()),
Err(err) => {
return rsx! {
h1 { "FATAL ERROR: {err}" }
}
}
}
rsx! {
Page {}
}
}
#[component]
fn Single(page: String) -> Element {
let site = meta::site();
let page_ref =
use_server_future(move || server::get_page(site.info.domain.clone(), page.clone()))?
.suspend()?;
match &*page_ref.read() {
Ok(page) => meta::set_page(page.clone()),
Err(err) => {
return rsx! {
h1 { "FATAL ERROR: {err}" }
}
}
}
rsx! {
Page {}
}
}
#[component]
fn Post(page: String, id: String) -> Element {
let site = meta::site();
let domain = site.info.domain.clone();
let page_ref =
use_server_future(move || server::get_page(domain.clone(), page.clone()))?.suspend()?;
match &*page_ref.read() {
Ok(page) => {
meta::set_page(page.clone());
}
Err(err) => {
return rsx! {
h1 { "FATAL ERROR: {err}" }
}
}
};
let page = meta::page();
let collection_id = match &page.data.content {
PageContent::Collection {
kind: _,
collection_id,
} => collection_id.clone(),
_ => {
return rsx! {
h1 { "NOT A COLLECTION" }
}
}
};
let post_ref = use_server_future(move || {
server::get_post(site.info.domain.clone(), collection_id.clone(), id.clone())
})?
.suspend()?;
let post = match &*post_ref.read() {
Ok(post) => post.clone(),
Err(err) => {
return rsx! {
h1 { "FATAL ERROR: {err}" }
}
}
};
rsx! {
PostElement { post }
}
}
#[cfg(test)]
mod tests {
use mockall::predicate;
use crate::{
domain::entities,
outbound::{
config::AppConfig,
services::site::{MockSiteService, SiteServiceProvider},
},
};
use super::*;
#[test]
fn gets_correct_site_info() {
let mut app = VirtualDom::new(|| {
rsx! {
App {}
}
});
let mut mock_service = MockSiteService::new();
mock_service.expect_get_site().times(1).returning(move |_| {
Box::pin(async {
Ok(entities::site::SiteInfo {
title: "My test website".to_string(),
domain: "test.com".to_string(),
pages: vec![],
})
})
});
server_context().insert(AppConfig {
admin_host: "admin.local".to_string(),
});
server_context().insert(SiteServiceProvider::with(mock_service));
app.rebuild_in_place();
let elem_str = dioxus::ssr::render(&app);
assert!(elem_str.contains("My test website"));
}
#[test]
fn single_gets_page_info() {
let mut app = VirtualDom::new(|| {
rsx! {
Single { page: "pagename".to_string() }
}
});
testing::add_test_site_context(&mut app);
let mut mock_service = MockSiteService::new();
mock_service
.expect_get_page()
.times(1)
.with(predicate::eq("test"), predicate::eq("pagename"))
.returning(move |_, _| {
Box::pin(async {
Ok(entities::site::Page {
info: entities::site::PageInfo {
title: "Test page name".to_string(),
name: "test".to_string(),
order: 0,
},
content: PageContent::Single {
content: entities::site::Post {
blocks: vec![entities::site::Block::Text {
text: "test content".to_string(),
}],
},
},
})
})
});
server_context().insert(SiteServiceProvider::with(mock_service));
app.rebuild_in_place();
let elem_str = dioxus::ssr::render(&app);
assert!(elem_str.contains("Test page name"));
assert!(elem_str.contains("test content"));
}
#[test]
fn home_gets_page_info() {
let mut app = VirtualDom::new(|| {
rsx! {
Home {}
}
});
testing::add_test_site_context(&mut app);
let mut mock_service = MockSiteService::new();
mock_service
.expect_get_page()
.times(1)
.with(predicate::eq("test"), predicate::eq("/"))
.returning(move |_, _| {
Box::pin(async {
Ok(entities::site::Page {
info: entities::site::PageInfo {
title: "Test page name".to_string(),
name: "test".to_string(),
order: 0,
},
content: PageContent::Single {
content: entities::site::Post { blocks: vec![] },
},
})
})
});
server_context().insert(SiteServiceProvider::with(mock_service));
app.rebuild_in_place();
let elem_str = dioxus::ssr::render(&app);
assert!(elem_str.contains("Test page name"));
}
#[test]
fn redirects_to_admin_ui() {
let mut app = VirtualDom::new(|| {
rsx! {
App {}
}
});
server_context().insert(AppConfig {
admin_host: "admin.local".to_string(),
});
server_context()
.request_parts_mut()
.headers
.insert("host", "admin.local".parse().unwrap());
app.rebuild_in_place();
let elem_str = dioxus::ssr::render(&app);
println!("elem_str: {elem_str}");
assert!(elem_str.contains("Admin UI"));
}
}