users::page, users::created_at, nav gating, icons, misc

This commit is contained in:
2026-03-08 23:50:06 +01:00
parent 8d18c858b3
commit 4a4e97f7be
12 changed files with 129 additions and 18 deletions

View File

@@ -2,6 +2,7 @@ use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use chrono::{DateTime, NaiveDate};
use rusqlite::{OptionalExtension, ffi::SQLITE_CONSTRAINT_UNIQUE};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -101,6 +102,13 @@ impl User {
self.handle = new_handle;
Ok(())
}
pub fn created_at(&self) -> Option<NaiveDate> {
self.id
.get_timestamp()
.and_then(|ts| DateTime::from_timestamp(ts.to_unix().0 as i64, 0))
.map(|dt| dt.date_naive())
}
}
// DANGEROUS: AUTH

View File

@@ -1,2 +1,3 @@
pub mod marquee;
pub mod nav;
pub mod user_miniprofile;

View File

@@ -2,23 +2,25 @@ use maud::{Markup, PreEscaped, html};
use crate::{users::User, web::icons};
const LINKS: &[(&str, &str, &str)] = &[
("Dashboard", "/dashboard", icons::LAYOUT_DASHBOARD),
("Quotes", "#quotes", icons::SCROLL_TEXT),
("Photos", "#photos", icons::FILE_IMAGE),
("Persons", "#persons", icons::CONTACT),
("Tags", "#tags", icons::TAG),
("Users", "#users", icons::USERS),
("Logs", "#logs", icons::CLIPBOARD_CLOCK),
// (SHOWTEXT, LINK, ICON, REQUIRES_LOG_IN)
const LINKS: &[(&str, &str, &str, bool)] = &[
("Dashboard", "/dashboard", icons::LAYOUT_DASHBOARD, false),
("Quotes", "#quotes", icons::SCROLL_TEXT, false),
("Photos", "#photos", icons::FILE_IMAGE, false),
("Persons", "#persons", icons::CONTACT, false),
("Tags", "#tags", icons::TAG, false),
("Users", "/users", icons::USERS, true),
("Logs", "#logs", icons::CLIPBOARD_CLOCK, true),
];
pub fn nav(user: Option<User>, uri: &str) -> Markup {
pub fn nav(user: Option<&User>, uri: &str) -> Markup {
html!(
div class="flex items-center text-sm gap-4 border-b border-neutral-200/25 bg-neutral-200/5 px-4 py-2" {
a href="/dashboard" class="font-lora font-semibold hidden xs:block md:text-xl sm:mr-2" {"Mnemosyne"}
div class="w-px h-5 bg-neutral-200/15 hidden sm:block" {}
div class="flex flex-row" {
@for link in LINKS {
@if !link.3 || user.is_some() {
a href={(link.1)} class="flex flex-row px-2 py-1 rounded items-center gap-2 hover:bg-neutral-200/5 border border-transparent hover:border-neutral-200/25" {
@if uri.starts_with(link.1) {
div class="scale-[.75] text-neutral-300" {(PreEscaped(link.2))}
@@ -30,6 +32,7 @@ pub fn nav(user: Option<User>, uri: &str) -> Markup {
}
}
}
}
@if let Some(u) = user {

View File

@@ -0,0 +1,33 @@
use maud::{Markup, PreEscaped, html};
use crate::{users::User, web::icons};
pub fn user_miniprofile(u: &User) -> Markup {
let show_shield = u.is_infradmin() || u.is_systemuser();
html!(
a href=(format!("/users/{}", u.id))
class="w-70 border border-neutral-200/25 hover:border-neutral-200/50 bg-neutral-200/5 hover:bg-neutral-200/10 transition-colors rounded flex" {
div class="bg-neutral-200/10 text-neutral-300 font-semibold aspect-square flex items-center justify-center" {
(u.handle.as_str().chars().next().unwrap_or('?').to_uppercase())
}
div class="p-3" {
p class="text-semibold flex" {
(u.handle)
@if show_shield {
span class="scale-[.75] text-neutral-500"
title="This is a special internal user." {(PreEscaped(icons::SHIELD_USER))}
}
}
p class="text-xs text-neutral-500 flex items-center mt-1" {
@if show_shield {
span class="scale-[.5] -ml-1" {(PreEscaped(icons::SERVER))}
"System account"
} @else {
span class="scale-[.5] -ml-1" {(PreEscaped(icons::CALENDAR_1))}
(u.created_at().map_or("Unknown".into(), |d| d.to_string()))
}
}
}
}
)
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar1-icon lucide-calendar-1"><path d="M11 14h1v4"/><path d="M16 2v4"/><path d="M3 10h18"/><path d="M8 2v4"/><rect x="3" y="4" width="18" height="18" rx="2"/></svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -1,10 +1,13 @@
// Below icons sourced from https://lucide.dev
pub const ARROW_RIGHT: &str = include_str!("arrow-right.svg");
pub const CALENDAR_1: &str = include_str!("calendar-1.svg");
pub const CLIPBOARD_CLOCK: &str = include_str!("clipboard-clock.svg");
pub const CONTACT: &str = include_str!("contact.svg");
pub const FILE_IMAGE: &str = include_str!("file-image.svg");
pub const LAYOUT_DASHBOARD: &str = include_str!("layout-dashboard.svg");
pub const SCROLL_TEXT: &str = include_str!("scroll-text.svg");
pub const SERVER: &str = include_str!("server.svg");
pub const SHIELD_USER: &str = include_str!("shield-user.svg");
pub const TAG: &str = include_str!("tag.svg");
pub const USER: &str = include_str!("user.svg");
pub const USER_KEY: &str = include_str!("user-key.svg");

1
src/web/icons/server.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-server-icon lucide-server"><rect width="20" height="8" x="2" y="2" rx="2" ry="2"/><rect width="20" height="8" x="2" y="14" rx="2" ry="2"/><line x1="6" x2="6.01" y1="6" y2="6"/><line x1="6" x2="6.01" y1="18" y2="18"/></svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield-user-icon lucide-shield-user"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="M6.376 18.91a6 6 0 0 1 11.249.003"/><circle cx="12" cy="11" r="4"/></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -11,7 +11,7 @@ pub async fn page(req: Request) -> Markup {
base(
"Dashboard | Mnemosyne",
html!(
(nav(u, req.uri().path()))
(nav(u.as_ref(), req.uri().path()))
div class="text-6xl sm:text-8xl text-neutral-800/25 mt-16 text-center font-semibold font-lora select-none overflow-hidden" {"Mnemosyne"}
),

View File

@@ -4,12 +4,14 @@ use maud::{DOCTYPE, Markup, html};
pub mod dashboard;
pub mod index;
pub mod login;
pub mod users;
pub fn pages() -> Router {
Router::new()
.route("/", get(index::page))
.route("/login", get(login::page))
.route("/dashboard", get(dashboard::page))
.route("/users", get(users::page))
}
pub fn base(title: &str, inner: Markup) -> Markup {

58
src/web/pages/users.rs Normal file
View File

@@ -0,0 +1,58 @@
use axum::{
extract::Request,
response::{IntoResponse, Response},
};
use maud::{PreEscaped, html};
use crate::{
users::{
User,
auth::{AuthError, UserAuthenticate},
},
web::{
components::{nav::nav, user_miniprofile::user_miniprofile},
icons,
pages::base,
},
};
pub async fn page(req: Request) -> Result<Response, AuthError> {
let u = User::authenticate(req.headers())?;
let us = match u.is_some() {
true => User::get_all(),
false => Ok(vec![]),
};
Ok(base(
"Users | Mnemosyne",
html!(
(nav(u.as_ref(), req.uri().path()))
@if let Some(_) = u {
div class="mx-auto max-w-4xl px-2 my-4" {
p class="flex items-center gap-2" {
span class="text-neutral-500" {(PreEscaped(icons::USERS))}
span class="text-2xl font-semibold font-lora" {"Users"}
}
p class="text-neutral-500 text-sm font-light" {
@if let Ok(v) = &us {
(v.len()) " users registered with Mnemosyne."
}
}
}
div class="mx-auto max-w-4xl flex flex-wrap gap-4" {
@if let Ok(vec) = &us {
@for user in vec {
(user_miniprofile(user))
}
} @else {
p class="text-center py-4 text-light text-red-500" {"Failed to load users."}
}
}
} @else {
p class="text-center p-2" {"You must be logged in to view this page."}
}
),
)
.into_response())
}

File diff suppressed because one or more lines are too long