From 14abdc9e4a740ef084c8e6fda9e86167b8c72db5 Mon Sep 17 00:00:00 2001 From: jmanczak Date: Thu, 9 Apr 2026 16:47:55 +0200 Subject: [PATCH] person profile page, adding names --- src/persons/mod.rs | 2 +- src/web/pages/mod.rs | 4 ++ src/web/pages/persons.rs | 4 +- src/web/pages/persons/profile.rs | 111 +++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 src/web/pages/persons/profile.rs diff --git a/src/persons/mod.rs b/src/persons/mod.rs index 4954e0f..9c7d702 100644 --- a/src/persons/mod.rs +++ b/src/persons/mod.rs @@ -86,7 +86,7 @@ impl Person { pub fn get_all_names(&self, conn: &Connection) -> Result, PersonError> { Ok(conn - .prepare("SELECT id, is_primary, person_id, created_by, name FROM names WHERE person_id = ?1")? + .prepare("SELECT id, is_primary, person_id, created_by, name FROM names WHERE person_id = ?1 ORDER BY id")? .query_map((&self.id,), |r| { Ok(Name { id: r.get(0)?, diff --git a/src/web/pages/mod.rs b/src/web/pages/mod.rs index 8107f8a..7cbf278 100644 --- a/src/web/pages/mod.rs +++ b/src/web/pages/mod.rs @@ -29,8 +29,12 @@ pub fn pages() -> Router { .route("/users/create-form", post(users::create::create_user)) .route("/tags", get(tags::page)) .route("/tags/create", post(tags::create)) + // .route("/persons", get(persons::page)) .route("/persons/create", post(persons::create)) + .route("/persons/{id}", get(persons::profile::page)) + .route("/persons/{id}/add-name", post(persons::profile::add_name)) + // .route("/logs", get(logs::page)) // .route("/quotes", get(quotes::page)) diff --git a/src/web/pages/persons.rs b/src/web/pages/persons.rs index 8a9cd7c..15daa64 100644 --- a/src/web/pages/persons.rs +++ b/src/web/pages/persons.rs @@ -19,6 +19,8 @@ use crate::{ web::{components::nav::nav, icons, pages::base}, }; +pub mod profile; + pub async fn page(req: Request) -> Result { let u = match User::authenticate(req.headers())? { Some(u) => u, @@ -48,7 +50,7 @@ pub async fn page(req: Request) -> Result { @if let Ok(persons) = Person::get_all(&tx) { div class="max-w-4xl mx-auto px-2 mt-4 flex flex-wrap gap-2" { @for person in &persons { - div class="rounded px-4 py-2 bg-neutral-200/10 border border-neutral-200/15 flex items-center" { + a href={"/persons/"(person.id)} class="rounded px-4 py-2 bg-neutral-200/5 hover:bg-neutral-200/10 border border-neutral-200/25 hover:border-neutral-200/25 flex items-center" { span class="text-neutral-400 mr-1 scale-125" {"~"} span class="text-sm" {(person.primary_name)} div class="w-px h-2/3 my-auto mx-2 bg-neutral-200/15" {} diff --git a/src/web/pages/persons/profile.rs b/src/web/pages/persons/profile.rs new file mode 100644 index 0000000..e9728a7 --- /dev/null +++ b/src/web/pages/persons/profile.rs @@ -0,0 +1,111 @@ +use axum::{ + Form, + extract::{Path, Request}, + http::HeaderMap, + response::{IntoResponse, Redirect, Response}, +}; +use maud::html; +use serde::Deserialize; +use uuid::Uuid; + +use crate::{ + database, + error::CompositeError, + logs::{LogAction, LogEntry}, + persons::Person, + users::{ + User, + auth::{AuthError, UserAuthRequired, UserAuthenticate}, + }, + web::{components::nav::nav, pages::base}, +}; + +pub async fn page(Path(id): Path, req: Request) -> Result { + let u = match User::authenticate(req.headers())? { + Some(u) => u, + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), + }; + let conn = database::conn()?; + let p = Person::get_by_id(&conn, id); + let title = match &p { + Ok(p) => format!("~{} | Mnemosyne", p.primary_name), + Err(_) => "Error!".into(), + }; + + Ok(base( + &title, + html!( + (nav(Some(&u), req.uri().path())) + + div class="mx-auto max-w-4xl px-2 my-4" { + @if let Ok(p) = p { + div class="flex items-center gap-2 mb-6" { + span class="text-neutral-500 scale-150" {"~"} + h1 class="text-3xl font-semibold font-lora" {(p.primary_name)} + } + div { + h2 class="text-lg font-semibold font-lora mb-2 text-neutral-300" {"Names"} + div class="flex flex-wrap gap-2 mb-4" { + @if let Ok(names) = p.get_all_names(&conn) { + @for name in &names { + div class="rounded px-3 py-1 bg-neutral-200/5 border border-neutral-200/10 text-sm flex items-center gap-2" { + (name.name) + @if name.is_primary { + span class="text-xs text-neutral-500" {"(primary)"} + } + } + } + } @else { + "Failed to get names." + } + } + form action=(format!("/persons/{}/add-name", p.id)) method="post" { + label for="name" class="text-neutral-500 font-light text-sm" {"Add Name"} + div class="flex gap-2 mt-1" { + input type="text" autocomplete="off" id="name" name="name" placeholder="e.g. Frank" + class="px-2 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded w-full sm:w-auto"; + button type="submit" + class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:bg-neutral-200/10 hover:border-neutral-200/45" {"Add"} + } + } + } + } @else { + p class="text-center p-2 my-4 text-red-400" {"Person not found."} + } + } + ), + ) + .into_response()) +} + +#[derive(Deserialize)] +pub struct AddNameForm { + name: String, +} + +pub async fn add_name( + Path(id): Path, + headers: HeaderMap, + Form(form): Form, +) -> Result { + let u = User::authenticate(&headers)?.required()?; + let mut conn = database::conn()?; + let tx = conn.transaction()?; + + let p = Person::get_by_id(&tx, id)?; + let n = p.add_name(&tx, form.name, u.id)?; + + LogEntry::new( + &tx, + u, + LogAction::AddPersonName { + pid: p.id, + nid: n.id, + pn: p.primary_name, + nn: n.name, + }, + )?; + tx.commit()?; + + Ok(Redirect::to(&format!("/persons/{}", p.id)).into_response()) +}