use actix_web::{HttpRequest, HttpResponse, Responder, get, web};
use crate::session::SessionStore;
use crate::view::guard::guard_view;
use crate::view::icons;
use crate::view::{is_htmx, main_with_content};
pub fn notify_content() -> maud::Markup {
maud::html! {
div class="flex min-h-screen w-full flex-col" {
nav class="navbar bg-base-100 shadow-sm border-b border-base-200 px-4" {
div class="navbar-start" {
a
href="/"
class="btn btn-ghost text-xl"
hx-get="/notify"
hx-target="#main"
hx-swap="innerHTML"
hx-push-url="/" {
"Notify"
}
}
div class="navbar-end gap-2" {
a
href="/install"
class="btn btn-ghost btn-sm"
hx-get="/install"
hx-target="#main"
hx-swap="innerHTML"
hx-push-url="true" {
"Install"
}
a
href="/debug"
class="btn btn-ghost btn-sm"
hx-get="/debug"
hx-target="#main"
hx-swap="innerHTML"
hx-push-url="true" {
"Debug"
}
}
}
div class="hero flex-1" {
div class="hero-content w-full justify-center p-4" {
div class="card card-border w-full max-w-sm bg-base-200" {
div class="card-body p-6" {
h1 class="card-title text-2xl font-bold justify-center text-center mb-2" {
"Notify"
}
div
class="card-actions flex flex-col gap-3 w-full"
hx-on="htmx:after:request -> { const ctx = event.detail.ctx; if (ctx.response.status < 400) { const feedback = document.getElementById('notify-feedback'); feedback.textContent = 'Sent ✓'; feedback.classList.remove('send-feedback-visible'); void feedback.offsetWidth; feedback.classList.add('send-feedback-visible'); setTimeout(() => { feedback.textContent = ''; feedback.classList.remove('send-feedback-visible'); }, 1800); } }" {
// to tell me I should look on my phone
button
type="button"
class="btn btn-secondary btn-lg btn-block"
hx-post="/notify/heart"
hx-swap="none"
hx-disabled-elt="this" {
span class="loading loading-spinner loading-md htmx-indicator" {}
span class="htmx-indicator font-semibold" { "Pending..." }
span class="htmx-hide-on-request flex items-center justify-center gap-2" {
(icons::heart())
span { "Look at your phone" }
}
}
//if my wife misses me
button
type="button"
class="btn btn-lg btn-block"
hx-post="/notify/cat"
hx-swap="none"
hx-disabled-elt="this" {
span class="loading loading-spinner loading-md htmx-indicator" {}
span class="htmx-indicator font-semibold" { "Pending..." }
span class="htmx-hide-on-request flex items-center justify-center gap-2" {
(icons::cat())
span { "Missing you" }
}
}
//when I need to call back now
button
type="button"
class="btn btn-error btn-lg btn-block"
hx-post="/notify/emergency"
hx-swap="none"
hx-disabled-elt="this" {
span class="loading loading-spinner loading-md htmx-indicator" {}
span class="htmx-indicator font-semibold" { "Pending..." }
span class="htmx-hide-on-request flex items-center justify-center gap-2" {
(icons::emergency())
span { "Emergency" }
}
}
}
p id="notify-feedback" class="send-feedback" role="status" aria-live="polite" {}
}
}
}
}
}
}
}
#[get("/notify")]
pub async fn notify(req: HttpRequest, session_store: web::Data<SessionStore>) -> impl Responder {
if session_store.is_authenticated(&req) {
let content = notify_content();
if is_htmx(&req) {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(content.into_string())
} else {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(main_with_content(content).into_string())
}
} else {
let guard = guard_view(None);
if is_htmx(&req) {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(guard.into_string())
} else {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(main_with_content(guard).into_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{App, cookie::Cookie, test as aw_test};
#[actix_web::test]
async fn test_notify_unauthenticated_returns_guard() {
let session_store = web::Data::new(SessionStore::in_memory());
let app =
aw_test::init_service(App::new().app_data(session_store.clone()).service(notify)).await;
let req = aw_test::TestRequest::get().uri("/notify").to_request();
let resp = aw_test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body = aw_test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("class=\"otp\""));
assert!(body_str.contains("Unlock"));
assert!(body_str.contains("<!DOCTYPE html>"));
// HTMX request
let req_htmx = aw_test::TestRequest::get()
.uri("/notify")
.insert_header(("HX-Request", "true"))
.to_request();
let resp_htmx = aw_test::call_service(&app, req_htmx).await;
assert!(resp_htmx.status().is_success());
let body_htmx = aw_test::read_body(resp_htmx).await;
let body_htmx_str = String::from_utf8(body_htmx.to_vec()).unwrap();
assert!(!body_htmx_str.contains("<!DOCTYPE html>"));
assert!(body_htmx_str.contains("class=\"otp\""));
}
#[actix_web::test]
async fn test_notify_authenticated_returns_content() {
let session_store = web::Data::new(SessionStore::in_memory());
let token = session_store.create_session().await;
let cookie = Cookie::build(crate::session::SESSION_COOKIE_NAME, token).finish();
let app =
aw_test::init_service(App::new().app_data(session_store.clone()).service(notify)).await;
let req = aw_test::TestRequest::get()
.uri("/notify")
.cookie(cookie.clone())
.to_request();
let resp = aw_test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body = aw_test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("<!DOCTYPE html>"));
assert!(body_str.contains("Look at your phone"));
assert!(body_str.contains("Missing you"));
assert!(body_str.contains("Emergency"));
// Check HTMX pending state elements
assert!(body_str.contains("loading loading-spinner"));
assert!(body_str.contains("htmx-indicator"));
assert!(body_str.contains("Pending..."));
assert!(body_str.contains("id=\"notify-feedback\""));
assert!(body_str.contains("htmx:after:request"));
assert!(body_str.contains("hx-disabled-elt=\"this\""));
assert!(body_str.contains("hx-swap=\"none\""));
// Check navigation and debug button placement
assert!(body_str.contains("class=\"navbar"));
assert!(body_str.contains("navbar-end"));
assert!(body_str.contains("href=\"/install\""));
assert!(body_str.contains("Install"));
assert!(body_str.contains("href=\"/debug\""));
assert!(body_str.contains("Debug"));
assert!(!body_str.contains("download=\"notify.json\""));
assert!(!body_str.contains("Export config"));
assert!(!body_str.contains("absolute top-4 right-4"));
// HTMX request returns partial content
let req_htmx = aw_test::TestRequest::get()
.uri("/notify")
.cookie(cookie)
.insert_header(("HX-Request", "true"))
.to_request();
let resp_htmx = aw_test::call_service(&app, req_htmx).await;
assert!(resp_htmx.status().is_success());
let body_htmx = aw_test::read_body(resp_htmx).await;
let body_htmx_str = String::from_utf8(body_htmx.to_vec()).unwrap();
assert!(!body_htmx_str.contains("<!DOCTYPE html>"));
assert!(body_htmx_str.contains("Look at your phone"));
}
}