This commit is contained in:
JMARyA 2025-01-21 16:39:47 +01:00
parent e02def6bc1
commit f3880d77d2
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
8 changed files with 265 additions and 85 deletions

View file

@ -31,4 +31,3 @@ reqwest = { version = "0.11", features = ["blocking"] }
[features] [features]
cache = [] cache = []
htmx = []

View file

@ -1,7 +1,7 @@
use based::get_pg; use based::get_pg;
use based::request::{RequestContext, StringResponse}; use based::request::{RequestContext, StringResponse};
use based::ui::components::Shell; use based::ui::components::Shell;
use based::ui::render_page; use based::ui::prelude::Nothing;
use maud::html; use maud::html;
use rocket::get; use rocket::get;
use rocket::routes; use rocket::routes;
@ -12,13 +12,9 @@ pub async fn index_page(ctx: RequestContext) -> StringResponse {
h1 { "Hello World!" }; h1 { "Hello World!" };
); );
render_page( Shell::new(Nothing(), Nothing(), Nothing())
content, .render_page(content, "Hello World", ctx)
"Hello World", .await
ctx,
&Shell::new(html! {}, html! {}, Some(String::new())),
)
.await
} }
#[rocket::launch] #[rocket::launch]

View file

@ -1,14 +1,15 @@
use based::asset::AssetRoutes;
use based::request::{RequestContext, StringResponse}; use based::request::{RequestContext, StringResponse};
use based::ui::components::{AppBar, Shell}; use based::ui::components::{AppBar, Shell};
use based::ui::htmx::{Event, HTMXAttributes}; use based::ui::htmx::{Event, HTMXAttributes};
use based::ui::{prelude::*, render_page}; use based::ui::prelude::*;
use maud::Render; use maud::Render;
use maud::html; use maud::html;
use rocket::get;
use rocket::routes; use rocket::routes;
use rocket::{State, get};
#[get("/")] #[get("/")]
pub async fn index_page(ctx: RequestContext) -> StringResponse { pub async fn index_page(ctx: RequestContext, shell: &State<Shell>) -> StringResponse {
let content = AppBar("MyApp", None).render(); let content = AppBar("MyApp", None).render();
let content = html!( let content = html!(
@ -36,19 +37,7 @@ pub async fn index_page(ctx: RequestContext) -> StringResponse {
); );
render_page( shell.render_page(content, "Hello World", ctx).await
content,
"Hello World",
ctx,
&Shell::new(
html! {
script src="https://cdn.tailwindcss.com" {};
},
html! {},
Some(String::new()),
),
)
.await
} }
#[rocket::launch] #[rocket::launch]
@ -56,5 +45,10 @@ async fn launch() -> _ {
// Logging // Logging
env_logger::init(); env_logger::init();
rocket::build().mount("/", routes![index_page]) let shell = Shell::new(Nothing(), Nothing(), Nothing()).use_ui();
rocket::build()
.mount("/", routes![index_page])
.mount_assets() // Mount included assets routes
.manage(shell) // Manage global shell reference
} }

View file

@ -1,5 +1,5 @@
use crate::request::assets::DataResponse; use crate::request::assets::DataResponse;
use rocket::get; use rocket::{Build, get, routes};
#[get("/assets/htmx.min.js")] #[get("/assets/htmx.min.js")]
pub fn htmx_script_route() -> DataResponse { pub fn htmx_script_route() -> DataResponse {
@ -9,3 +9,13 @@ pub fn htmx_script_route() -> DataResponse {
Some(60 * 60 * 24 * 3), Some(60 * 60 * 24 * 3),
) )
} }
pub trait AssetRoutes {
fn mount_assets(self) -> Self;
}
impl AssetRoutes for rocket::Rocket<Build> {
fn mount_assets(self) -> Self {
self.mount("/", routes![crate::asset::htmx_script_route])
}
}

View file

@ -2,10 +2,9 @@
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
pub mod asset;
pub mod auth; pub mod auth;
pub mod format; pub mod format;
#[cfg(feature = "htmx")]
pub mod htmx;
pub mod request; pub mod request;
pub mod result; pub mod result;
pub mod ui; pub mod ui;

View file

@ -1,5 +1,10 @@
use maud::{PreEscaped, html}; use maud::{PreEscaped, html};
use crate::{
request::{RequestContext, StringResponse},
ui::UIWidget,
};
// TODO : refactor shell // TODO : refactor shell
/// Represents the HTML structure of a page shell, including the head, body class, and body content. /// Represents the HTML structure of a page shell, including the head, body class, and body content.
@ -9,9 +14,10 @@ pub struct Shell {
/// The HTML content for the `<head>` section of the page. /// The HTML content for the `<head>` section of the page.
head: PreEscaped<String>, head: PreEscaped<String>,
/// An optional class attribute for the `<body>` element. /// An optional class attribute for the `<body>` element.
body_class: Option<String>, body_class: String,
/// The HTML content for the static body portion. /// The HTML content for the static body portion.
body_content: PreEscaped<String>, body_content: PreEscaped<String>,
ui: bool,
} }
impl Shell { impl Shell {
@ -25,18 +31,24 @@ impl Shell {
/// # Returns /// # Returns
/// A `Shell` instance encapsulating the provided HTML content and attributes. /// A `Shell` instance encapsulating the provided HTML content and attributes.
#[must_use] #[must_use]
pub const fn new( pub fn new<T: UIWidget + 'static, C: UIWidget + 'static, B: UIWidget + 'static>(
head: PreEscaped<String>, head: T,
body_content: PreEscaped<String>, body_content: B,
body_class: Option<String>, body_class: C,
) -> Self { ) -> Self {
Self { Self {
head, head: head.render(),
body_class, body_class: body_class.extended_class().join(" "),
body_content, body_content: body_content.render(),
ui: false,
} }
} }
pub fn use_ui(mut self) -> Self {
self.ui = true;
self
}
/// Renders the full HTML page using the shell structure, with additional content and a title. /// Renders the full HTML page using the shell structure, with additional content and a title.
/// ///
/// # Arguments /// # Arguments
@ -51,10 +63,15 @@ impl Shell {
html { html {
head { head {
title { (title) }; title { (title) };
@if self.ui {
script src="https://cdn.tailwindcss.com" {};
script src="/assets/htmx.min.js" {};
meta name="viewport" content="width=device-width, initial-scale=1.0";
};
(self.head) (self.head)
}; };
@if self.body_class.is_some() { @if !self.body_class.is_empty() {
body class=(self.body_class.as_ref().unwrap()) { body class=(self.body_class) {
(self.body_content); (self.body_content);
div id="main_content" { div id="main_content" {
@ -73,4 +90,38 @@ impl Shell {
} }
} }
} }
/// Renders a full page or an HTMX-compatible fragment based on the request context.
///
/// If the request is not an HTMX request, this function uses the provided shell to generate
/// a full HTML page. If it is an HTMX request, only the provided content is rendered.
///
/// # Arguments
/// * `content` - The HTML content to render.
/// * `title` - The title of the page for full-page rendering.
/// * `ctx` - The `RequestContext` containing request metadata.
///
/// # Returns
/// A `StringResponse`
pub async fn render_page(
&self,
content: PreEscaped<String>,
title: &str,
ctx: RequestContext,
) -> StringResponse {
if ctx.is_htmx {
(
rocket::http::Status::Ok,
(rocket::http::ContentType::HTML, content.into_string()),
)
} else {
(
rocket::http::Status::Ok,
(
rocket::http::ContentType::HTML,
self.render(content, title).into_string(),
),
)
}
}
} }

View file

@ -1,4 +1,3 @@
use components::Shell;
use maud::{Markup, PreEscaped, Render, html}; use maud::{Markup, PreEscaped, Render, html};
// UI // UI
@ -71,50 +70,6 @@ pub mod prelude {
}; };
} }
use crate::request::{RequestContext, StringResponse};
use rocket::http::{ContentType, Status};
/// Renders a full page or an HTMX-compatible fragment based on the request context.
///
/// If the request is not an HTMX request, this function uses the provided shell to generate
/// a full HTML page. If it is an HTMX request, only the provided content is rendered.
///
/// # Arguments
/// * `content` - The HTML content to render.
/// * `title` - The title of the page for full-page rendering.
/// * `ctx` - The `RequestContext` containing request metadata.
/// * `shell` - The `Shell` instance used for full-page rendering.
///
/// # Returns
/// A `StringResponse`
pub async fn render_page(
content: PreEscaped<String>,
title: &str,
ctx: RequestContext,
shell: &Shell,
) -> StringResponse {
if ctx.is_htmx {
(Status::Ok, (ContentType::HTML, content.into_string()))
} else {
(
Status::Ok,
(
ContentType::HTML,
shell.render(content, title).into_string(),
),
)
}
}
// Grids
// ListViews
// ListTiles
// Cards
/// Generic UI Widget /// Generic UI Widget
pub trait UIWidget: Render { pub trait UIWidget: Render {
/// Indicating if the widget supports inheriting classes /// Indicating if the widget supports inheriting classes

View file

@ -1,5 +1,5 @@
use crate::ui::UIWidget; use crate::ui::UIWidget;
use maud::{Markup, Render, html}; use maud::{Markup, PreEscaped, Render, html};
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[must_use] #[must_use]
@ -48,3 +48,179 @@ impl UIWidget for ImageWidget {
} }
} }
} }
#[allow(non_snake_case)]
#[must_use]
pub fn Video() -> VideoWidget {
VideoWidget {
src: Vec::new(),
controls: false,
autoplay: false,
looping: false,
muted: false,
poster: None,
width: None,
height: None,
}
}
pub struct VideoWidget {
src: Vec<SourceWidget>,
controls: bool,
autoplay: bool,
looping: bool,
muted: bool,
poster: Option<String>,
width: Option<u32>,
height: Option<u32>,
}
impl Render for VideoWidget {
fn render(&self) -> Markup {
self.render_with_class("")
}
}
impl VideoWidget {
pub fn add_src(mut self, src: SourceWidget) -> Self {
self.src.push(src);
self
}
pub fn controls(mut self) -> Self {
self.controls = true;
self
}
pub fn autoplay(mut self) -> Self {
self.autoplay = true;
self
}
pub fn looping(mut self) -> Self {
self.looping = true;
self
}
pub fn muted(mut self) -> Self {
self.muted = true;
self
}
pub fn poster(mut self, poster: &str) -> Self {
self.poster = Some(poster.to_string());
self
}
pub fn width(mut self, w: u32) -> Self {
self.width = Some(w);
self
}
pub fn height(mut self, h: u32) -> Self {
self.height = Some(h);
self
}
}
impl UIWidget for VideoWidget {
fn can_inherit(&self) -> bool {
true
}
fn base_class(&self) -> Vec<String> {
vec![]
}
fn extended_class(&self) -> Vec<String> {
self.base_class()
}
fn render_with_class(&self, class: &str) -> Markup {
let mut ret = "<video".to_string();
if self.controls {
ret.push_str(" controls");
}
if self.autoplay {
ret.push_str(" autoplay");
}
if self.looping {
ret.push_str(" loop");
}
if self.muted {
ret.push_str(" muted");
}
if let Some(poster) = &self.poster {
ret.push_str(&format!(" poster=\"{}\"", poster.replace("\"", "\\\"")));
}
if let Some(w) = &self.width {
ret.push_str(&format!(" width=\"{}\"", w));
}
if let Some(h) = &self.height {
ret.push_str(&format!(" height=\"{}\"", h));
}
ret.push_str(&format!(" class=\"{class}\""));
ret.push_str("> ");
for src in &self.src {
ret.push_str(&src.render().0);
}
ret.push_str("\nYour browser does not support the video tag.\n</video>");
PreEscaped(ret)
}
}
#[allow(non_snake_case)]
#[must_use]
pub fn Source(src: &str, mime: Option<String>) -> SourceWidget {
SourceWidget {
src: src.to_owned(),
mime,
}
}
pub struct SourceWidget {
src: String,
mime: Option<String>,
}
impl Render for SourceWidget {
fn render(&self) -> Markup {
self.render_with_class("")
}
}
impl UIWidget for SourceWidget {
fn can_inherit(&self) -> bool {
false
}
fn base_class(&self) -> Vec<String> {
vec![]
}
fn extended_class(&self) -> Vec<String> {
self.base_class()
}
fn render_with_class(&self, _: &str) -> Markup {
html! {
@if let Some(mime) = &self.mime {
source src=(self.src) type=(mime);
} else {
source src=(self.src);
};
}
}
}