Add hx-boosted support to Layout

It will now skip rendering the layout if the request is coming from an
hx-boosted link or form and only update the head title with a
hx-swap-oob.
This commit is contained in:
2023-09-26 23:36:31 -04:00
parent 81b4ef860e
commit 092a38ad52
12 changed files with 191 additions and 78 deletions

View File

@@ -2,11 +2,13 @@ use std::fs;
use axum::extract::{Path, State};
use axum::response::Response;
use axum::TypedHeader;
use maud::{html, PreEscaped};
use sqlx::PgPool;
use crate::config::Config;
use crate::error::Result;
use crate::htmx::HXBoosted;
use crate::models::entry::Entry;
use crate::partials::layout::Layout;
use crate::uuid::Base62Uuid;
@@ -15,6 +17,7 @@ pub async fn get(
Path(id): Path<Base62Uuid>,
State(pool): State<PgPool>,
State(config): State<Config>,
hx_boosted: Option<TypedHeader<HXBoosted>>,
layout: Layout,
) -> Result<Response> {
let entry = Entry::get(&pool, id.as_uuid()).await?;
@@ -25,20 +28,23 @@ pub async fn get(
.published_at
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let content = fs::read_to_string(content_path).unwrap_or_else(|_| "No content".to_string());
Ok(layout.with_subtitle(&title).render(html! {
article {
header {
h2 class="title" { a href=(entry.url) { (title) } }
}
div {
span class="published" {
strong { "Published: " }
time datetime=(published_at) class="local-time" {
(published_at)
Ok(layout
.with_subtitle(&title)
.boosted(hx_boosted)
.render(html! {
article {
header {
h2 class="title" { a href=(entry.url) { (title) } }
}
div {
span class="published" {
strong { "Published: " }
time datetime=(published_at) class="local-time" {
(published_at)
}
}
}
(PreEscaped(content))
}
(PreEscaped(content))
}
}))
}))
}

View File

@@ -4,7 +4,7 @@ use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::sse::{Event, KeepAlive};
use axum::response::{IntoResponse, Redirect, Response, Sse};
use axum::Form;
use axum::{Form, TypedHeader};
use feed_rs::parser;
use maud::html;
use serde::Deserialize;
@@ -16,6 +16,7 @@ use tokio_stream::StreamExt;
use crate::actors::crawl_scheduler::{CrawlSchedulerHandle, CrawlSchedulerHandleMessage};
use crate::actors::feed_crawler::FeedCrawlerHandleMessage;
use crate::error::{Error, Result};
use crate::htmx::HXBoosted;
use crate::models::entry::{Entry, GetEntriesOptions};
use crate::models::feed::{CreateFeed, Feed};
use crate::partials::add_feed_form::add_feed_form;
@@ -27,6 +28,7 @@ use crate::uuid::Base62Uuid;
pub async fn get(
Path(id): Path<Base62Uuid>,
State(pool): State<PgPool>,
hx_boosted: Option<TypedHeader<HXBoosted>>,
layout: Layout,
) -> Result<Response> {
let feed = Feed::get(&pool, id.as_uuid()).await?;
@@ -37,7 +39,7 @@ pub async fn get(
let title = feed.title.unwrap_or_else(|| "Untitled Feed".to_string());
let entries = Entry::get_all(&pool, &options).await?;
let delete_url = format!("/feed/{}/delete", id);
Ok(layout.with_subtitle(&title).render(html! {
Ok(layout.with_subtitle(&title).boosted(hx_boosted).render(html! {
header class="feed-header" {
h2 { (title) }
button class="edit-feed" { "✏️ Edit feed" }

View File

@@ -1,29 +1,38 @@
use axum::extract::State;
use axum::response::Response;
use axum::TypedHeader;
use maud::html;
use sqlx::PgPool;
use crate::error::Result;
use crate::htmx::HXBoosted;
use crate::models::feed::{Feed, GetFeedsOptions};
use crate::partials::add_feed_form::add_feed_form;
use crate::partials::feed_list::feed_list;
use crate::partials::layout::Layout;
use crate::partials::opml_import_form::opml_import_form;
pub async fn get(State(pool): State<PgPool>, layout: Layout) -> Result<Response> {
pub async fn get(
State(pool): State<PgPool>,
hx_boosted: Option<TypedHeader<HXBoosted>>,
layout: Layout,
) -> Result<Response> {
let options = GetFeedsOptions::default();
let feeds = Feed::get_all(&pool, &options).await?;
Ok(layout.with_subtitle("feeds").render(html! {
header { h2 { "Feeds" } }
div class="feeds" {
ul id="feeds" {
(feed_list(feeds, &options))
Ok(layout
.with_subtitle("feeds")
.boosted(hx_boosted)
.render(html! {
header { h2 { "Feeds" } }
div class="feeds" {
ul id="feeds" {
(feed_list(feeds, &options))
}
div class="add-feed" {
h3 { "Add Feed" }
(add_feed_form())
(opml_import_form())
}
}
div class="add-feed" {
h3 { "Add Feed" }
(add_feed_form())
(opml_import_form())
}
}
}))
}))
}

View File

@@ -1,16 +1,22 @@
use axum::extract::State;
use axum::response::Response;
use axum::TypedHeader;
use maud::html;
use sqlx::PgPool;
use crate::error::Result;
use crate::htmx::HXBoosted;
use crate::models::entry::Entry;
use crate::partials::{layout::Layout, entry_list::entry_list};
use crate::partials::{entry_list::entry_list, layout::Layout};
pub async fn get(State(pool): State<PgPool>, layout: Layout) -> Result<Response> {
pub async fn get(
State(pool): State<PgPool>,
hx_boosted: Option<TypedHeader<HXBoosted>>,
layout: Layout,
) -> Result<Response> {
let options = Default::default();
let entries = Entry::get_all(&pool, &options).await?;
Ok(layout.render(html! {
Ok(layout.boosted(hx_boosted).render(html! {
ul class="entries" {
(entry_list(entries, &options))
}

View File

@@ -9,6 +9,7 @@ use axum::response::{
sse::{Event, Sse},
Response,
};
use axum::TypedHeader;
use bytes::Bytes;
use maud::{html, PreEscaped};
use tokio::sync::watch::Receiver;
@@ -17,16 +18,20 @@ use tokio_stream::Stream;
use tokio_stream::StreamExt;
use crate::error::Result;
use crate::htmx::HXBoosted;
use crate::log::MEM_LOG;
use crate::partials::layout::Layout;
pub async fn get(layout: Layout) -> Result<Response> {
pub async fn get(hx_boosted: Option<TypedHeader<HXBoosted>>, layout: Layout) -> Result<Response> {
let mem_buf = MEM_LOG.lock().unwrap();
Ok(layout.with_subtitle("log").render(html! {
pre id="log" hx-sse="connect:/log/stream swap:message" hx-swap="beforeend" {
(PreEscaped(convert_escaped(from_utf8(mem_buf.as_slices().0).unwrap()).unwrap()))
}
}))
Ok(layout
.with_subtitle("log")
.boosted(hx_boosted)
.render(html! {
pre id="log" hx-sse="connect:/log/stream swap:message" hx-swap="beforeend" hx-target="#log" {
(PreEscaped(convert_escaped(from_utf8(mem_buf.as_slices().0).unwrap()).unwrap()))
}
}))
}
pub async fn stream(

View File

@@ -1,4 +1,5 @@
use axum::response::{IntoResponse, Response};
use axum::TypedHeader;
use axum::{extract::State, Form};
use maud::html;
use serde::Deserialize;
@@ -7,7 +8,7 @@ use sqlx::PgPool;
use crate::auth::verify_password;
use crate::error::{Error, Result};
use crate::htmx::HXRedirect;
use crate::htmx::{HXBoosted, HXRedirect};
use crate::partials::login_form::{login_form, LoginFormProps};
use crate::{
models::user::{AuthContext, User},
@@ -21,13 +22,16 @@ pub struct Login {
password: String,
}
pub async fn get(layout: Layout) -> Result<Response> {
Ok(layout.with_subtitle("login").render(html! {
header {
h2 { "Login" }
}
(login_form(LoginFormProps::default()))
}))
pub async fn get(hx_boosted: Option<TypedHeader<HXBoosted>>, layout: Layout) -> Result<Response> {
Ok(layout
.with_subtitle("login")
.boosted(hx_boosted)
.render(html! {
header {
h2 { "Login" }
}
(login_form(LoginFormProps::default()))
}))
}
pub async fn post(

View File

@@ -1,4 +1,5 @@
use axum::response::{IntoResponse, Response};
use axum::TypedHeader;
use axum::{extract::State, Form};
use maud::html;
use serde::Deserialize;
@@ -6,7 +7,7 @@ use serde_with::{serde_as, NoneAsEmptyString};
use sqlx::PgPool;
use crate::error::{Error, Result};
use crate::htmx::HXRedirect;
use crate::htmx::{HXBoosted, HXRedirect};
use crate::models::user::{AuthContext, CreateUser, User};
use crate::partials::layout::Layout;
use crate::partials::signup_form::{signup_form, SignupFormProps};
@@ -21,13 +22,16 @@ pub struct Signup {
pub name: Option<String>,
}
pub async fn get(layout: Layout) -> Result<Response> {
Ok(layout.with_subtitle("signup").render(html! {
header {
h2 { "Signup" }
}
(signup_form(SignupFormProps::default()))
}))
pub async fn get(hx_boosted: Option<TypedHeader<HXBoosted>>, layout: Layout) -> Result<Response> {
Ok(layout
.with_subtitle("signup")
.boosted(hx_boosted)
.render(html! {
header {
h2 { "Signup" }
}
(signup_form(SignupFormProps::default()))
}))
}
pub async fn post(