use actix_web::Result as AwResult;
use actix_web::{HttpRequest, HttpResponse, get, web};
use super::render_layout;
use super::session_auth::get_username_from_request;
use crate::auth::NotesContext;
#[get("/")]
pub async fn index(
req: HttpRequest,
auth_state: web::Data<NotesContext>,
) -> AwResult<HttpResponse> {
let username = get_username_from_request(&req, &auth_state).await;
let username = match username {
Some(name) => name,
None => {
return Ok(HttpResponse::Found()
.insert_header(("Location", "/auth/login"))
.finish());
}
};
let content = maud::html! {
div class="mw8 center w-100 mt3" {
div class="tc mb5" {
h1 class="f1 fw6 brand mv3" { "Welcome, " (username) "!" }
p class="f4 black-60 mt0" { "What would you like to do today?" }
}
div class="flex flex-column flex-row-m flex-row-l justify-center gap-4 justify-around-ns" {
// Save Card
div class="w-100 w-45-m w-45-l bg-white pa4 shadow-4 ba b--black-10 note-card flex flex-column justify-between mb4 mb0-ns" {
div {
div class="flex items-center mb3" {
div class="bg-blue white pa3 flex items-center justify-center mr3" style="width: 48px; height: 48px;" {
// Simple pen SVG
svg class="w2 h2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {
path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" {}
}
}
h2 class="f3 fw6 ma0 brand" { "Save Note" }
}
p class="f5 black-60 lh-copy" {
"Jot down your thoughts, ideas, or reminders instantly. Add tags and categories to organize them. The title of the note is automatically derived from the start of your text!"
}
}
a href="/save" class="db tc btn-brand no-underline f5 fw6 mt3 pv3" {
"Go to Save Note"
}
}
// View Card
div class="w-100 w-45-m w-45-l bg-white pa4 shadow-4 ba b--black-10 note-card flex flex-column justify-between" {
div {
div class="flex items-center mb3" {
div class="bg-green white pa3 flex items-center justify-center mr3" style="width: 48px; height: 48px;" {
// Simple search SVG
svg class="w2 h2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {
path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" {}
}
}
h2 class="f3 fw6 ma0 brand" { "Search & View" }
}
p class="f5 black-60 lh-copy" {
"Browse through your notes library. Easily search by keywords, filter by categories and tags, and edit or delete your notes as needed."
}
}
a href="/view" class="db tc btn-brand no-underline f5 fw6 mt3 pv3" {
"Go to Search & View"
}
}
}
}
};
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(render_layout(&content, Some(&username)).into_string()))
}
#[get("/save")]
pub async fn save_page(
req: HttpRequest,
auth_state: web::Data<NotesContext>,
) -> AwResult<HttpResponse> {
let username = get_username_from_request(&req, &auth_state).await;
let username = match username {
Some(name) => name,
None => {
return Ok(HttpResponse::Found()
.insert_header(("Location", "/auth/login"))
.finish());
}
};
let content = maud::html! {
div class="mw7 center w-100 bg-white pa4 shadow-4 ba b--black-10 mt3" {
div class="flex items-center justify-between mb4 pb2 bb b--black-10" {
h1 class="f3 fw6 brand ma0" { "Create New Note" }
a href="/" class="link gray hover-black f6 underline" { "Back to Dashboard" }
}
form
hx-post="/save"
hx-target="#save-result"
hx-swap="innerHTML"
hx-on--after-request="if(event.detail.successful) this.reset()"
class="flex flex-column"
{
div class="mb3" {
label class="db f6 fw6 black-70 mb2" for="content" { "Note Content" }
textarea
name="content"
id="content"
required
rows="12"
class="db w-100 pa3 ba b--black-20 f5 code"
style="resize: vertical;"
placeholder="Start typing your note here... The first line will automatically become the title!" {}
}
div class="flex flex-column flex-row-ns gap-3 mb4" {
div class="w-100 w-50-ns mb3 mb0-ns mr2-ns" {
label class="db f6 fw6 black-70 mb2" for="category" { "Category (Optional)" }
input
type="text"
name="category"
id="category"
class="db w-100 pa2 ba b--black-20 f5"
placeholder="e.g. Work, Personal, Snippets";
}
div class="w-100 w-50-ns ml2-ns" {
label class="db f6 fw6 black-70 mb2" for="tags" { "Tags (Optional, comma-separated)" }
input
type="text"
name="tags"
id="tags"
class="db w-100 pa2 ba b--black-20 f5"
placeholder="e.g. rust, htmx, ideas";
}
}
button
type="submit"
class="btn-brand f5 fw6 cursor-pointer self-start ph4 pv3"
{
"Save Note"
}
}
div id="save-result" class="mt3" {}
}
};
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(render_layout(&content, Some(&username)).into_string()))
}
#[get("/view")]
pub async fn view_page(
req: HttpRequest,
auth_state: web::Data<NotesContext>,
) -> AwResult<HttpResponse> {
let username = get_username_from_request(&req, &auth_state).await;
let username = match username {
Some(name) => name,
None => {
return Ok(HttpResponse::Found()
.insert_header(("Location", "/auth/login"))
.finish());
}
};
let db = auth_state.db();
let categories = db.get_all_categories().await.unwrap_or_default();
let tags = db.get_all_tags().await.unwrap_or_default();
let content = maud::html! {
div class="mw9 center w-100 mt2 flex flex-column flex-row-ns" style="min-height: 75vh;" {
// Left column: Search and list of notes
div class="w-100 w-40-ns pr3-ns br-ns b--black-10 flex flex-column mb4 mb0-ns" {
(render_search_card(&categories, &tags))
// Search results container
div
id="search-results"
hx-get="/view/search"
hx-include="#search-form"
hx-trigger="load, refresh-list from:body"
class="flex-auto overflow-y-auto"
style="max-height: 60vh;"
{
p class="gray tc mt4" { "Loading notes..." }
}
}
// Right column: Selected note details / Edit form
div id="note-details" class="w-100 w-60-ns pl3-ns" {
div class="bg-white pa4 shadow-4 ba b--black-10 tc gray f5 flex flex-column justify-center items-center" style="min-height: 300px;" {
// Simple note icon
svg class="w3 h3 mb3 black-30" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {
path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" {}
}
"Select a note from the left to view and edit its details."
}
}
}
};
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(render_layout(&content, Some(&username)).into_string()))
}
pub fn render_search_card(categories: &[String], tags: &[String]) -> maud::Markup {
maud::html! {
div class="bg-white pa3 shadow-4 ba b--black-10 mb3" id="search-card" {
div class="flex items-center justify-between mb3" {
h2 class="f4 fw6 brand ma0" { "Search & Filter" }
button
hx-get="/view/search-card"
hx-target="#search-card"
hx-swap="outerHTML"
class="f6 link gray hover-brand bg-transparent bn pointer underline"
{
"Reload"
}
}
form
id="search-form"
hx-get="/view/search"
hx-target="#search-results"
hx-trigger="submit, change, input changed delay:500ms from:#search-input"
onsubmit="return false;"
class="flex flex-column"
{
div class="mb3" {
label class="db f6 fw6 black-70 mb1" for="search-input" { "Keyword Search" }
input
type="search"
name="q"
id="search-input"
class="db w-100 pa2 ba b--black-20 f5"
placeholder="Search title or content...";
}
div class="flex gap-2" {
div class="w-50 mr1" {
label class="db f6 fw6 black-70 mb1" for="category-select" { "Category" }
select
name="category"
id="category-select"
class="db w-100 pa2 ba b--black-20 f6 bg-white"
{
option value="" { "All Categories" }
@for cat in categories {
option value=(cat) { (cat) }
}
}
}
div class="w-50 ml1" {
label class="db f6 fw6 black-70 mb1" for="tag-select" { "Tag" }
select
name="tag"
id="tag-select"
class="db w-100 pa2 ba b--black-20 f6 bg-white"
{
option value="" { "All Tags" }
@for tag in tags {
option value=(tag) { (tag) }
}
}
}
}
}
}
}
}
#[get("/view/search-card")]
pub async fn search_card_handler(
req: HttpRequest,
auth_state: web::Data<NotesContext>,
) -> AwResult<HttpResponse> {
let username = get_username_from_request(&req, &auth_state).await;
let _username = match username {
Some(name) => name,
None => {
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
}
};
let db = auth_state.db();
let categories = db.get_all_categories().await.unwrap_or_default();
let tags = db.get_all_tags().await.unwrap_or_default();
let markup = render_search_card(&categories, &tags);
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(markup.into_string()))
}