Skip to content
use actix_web::HttpRequest;
use maud::{DOCTYPE, Markup, html};

pub mod auth;
mod form;
pub mod tickets;

pub fn is_htmx(req: &HttpRequest) -> bool {
    req.headers().contains_key("hx-request")
}

/// The full HTML document every page is wrapped in.
pub fn render_layout(
    main_content: &Markup,
    username: Option<&str>,
    page_title: Option<&str>,
) -> Markup {
    html! {
        (DOCTYPE)
        html lang="en" data-theme="light" {
            head {
                meta charset="utf-8";
                meta name="viewport" content="width=device-width, initial-scale=1.0";
                title {
                    @if let Some(page_title) = page_title {
                        (page_title) " · Pear"
                    } @else {
                        "Pear"
                    }
                }
                link rel="stylesheet" href="/assets/daisyui.css";
                link rel="stylesheet" href="/assets/app.css";
                meta name="htmx-config" content=(r#"{"implicitInheritance":true,"noSwap":[204,304]}"#);
                script src="/assets/h.js" {}
            }
            body {
                header class="site-header" {
                    div class="header-inner" {
                        a class="brand" href="/" aria-label="Pear home" { "Pear" }
                        nav class="primary-nav" aria-label="Main navigation" {
                            @if username.is_some() {
                                a href="/" { "Tasks" }
                                a href="/tickets/new" { "New task" }
                            }
                        }
                        div class="account" {
                            @match username {
                                Some(name) => {
                                    span class="account-name" { (name) }
                                    form method="POST" action="/auth/logout" {
                                        button class="link-button" type="submit" { "Log out" }
                                    }
                                }
                                None => {
                                    a class="btn btn-outline btn-sm" href="/auth/login" { "Log in" }
                                }
                            }
                        }
                    }
                }
                main class="page-shell" {
                    (main_content)
                }
            }
        }
    }
}

/// An announced error notice, swapped into a form's result target.
pub fn render_error(message: &str) -> Markup {
    html! {
        div class="alert alert-error mt-4" role="alert" {
            span { (message) }
        }
    }
}

/// An announced success notice.
pub fn render_success(message: &str) -> Markup {
    html! {
        div class="alert alert-success mt-4" role="status" {
            span { (message) }
        }
    }
}

/// Renders `content` as a bare partial for HTMX requests, or wraps it in the
/// full document for a normal browser navigation.
pub fn page_or_partial(
    req: &HttpRequest,
    content: Markup,
    username: Option<&str>,
    title: &str,
) -> Markup {
    if is_htmx(req) {
        content
    } else {
        render_layout(&content, username, Some(title))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_layout_contains_shell_and_assets() {
        let content = html! { p { "hello" } };
        let rendered = render_layout(&content, None, Some("Home")).into_string();

        assert!(rendered.contains("<!DOCTYPE html>"));
        assert!(rendered.contains("<title>Home · Pear</title>"));
        assert!(rendered.contains("/assets/daisyui.css"));
        assert!(rendered.contains("/assets/app.css"));
        assert!(rendered.contains("/assets/h.js"));
        assert!(rendered.contains("hello"));
    }

    #[test]
    fn test_layout_anonymous_nav_offers_login_without_open_signup() {
        let content = html! {};
        let rendered = render_layout(&content, None, None).into_string();
        assert!(rendered.contains("/auth/login"));
        assert!(!rendered.contains("/auth/signup"));
        assert!(!rendered.contains("/auth/logout"));
    }

    #[test]
    fn test_layout_authenticated_nav_offers_logout() {
        let content = html! {};
        let rendered = render_layout(&content, Some("alice"), None).into_string();
        assert!(rendered.contains("alice"));
        assert!(rendered.contains("/auth/logout"));
        assert!(!rendered.contains("href=\"/auth/login\""));
    }

    #[test]
    fn test_layout_authenticated_nav_links_to_new_task_page() {
        let content = html! {};
        let rendered = render_layout(&content, Some("alice"), None).into_string();
        assert!(rendered.contains("href=\"/tickets/new\""));
        assert!(!rendered.contains("/#new-task"));
    }

    #[test]
    fn test_notices_escape_message_markup() {
        for html in [
            render_error("<script>boom()</script>").into_string(),
            render_success("<script>boom()</script>").into_string(),
        ] {
            assert!(!html.contains("<script>"));
            assert!(html.contains("&lt;script&gt;"));
        }
    }
}