users::page, users::created_at, nav gating, icons, misc
This commit is contained in:
@@ -2,6 +2,7 @@ use axum::{
|
|||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use chrono::{DateTime, NaiveDate};
|
||||||
use rusqlite::{OptionalExtension, ffi::SQLITE_CONSTRAINT_UNIQUE};
|
use rusqlite::{OptionalExtension, ffi::SQLITE_CONSTRAINT_UNIQUE};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -101,6 +102,13 @@ impl User {
|
|||||||
self.handle = new_handle;
|
self.handle = new_handle;
|
||||||
Ok(())
|
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
|
// DANGEROUS: AUTH
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod marquee;
|
pub mod marquee;
|
||||||
pub mod nav;
|
pub mod nav;
|
||||||
|
pub mod user_miniprofile;
|
||||||
|
|||||||
@@ -2,30 +2,33 @@ use maud::{Markup, PreEscaped, html};
|
|||||||
|
|
||||||
use crate::{users::User, web::icons};
|
use crate::{users::User, web::icons};
|
||||||
|
|
||||||
const LINKS: &[(&str, &str, &str)] = &[
|
// (SHOWTEXT, LINK, ICON, REQUIRES_LOG_IN)
|
||||||
("Dashboard", "/dashboard", icons::LAYOUT_DASHBOARD),
|
const LINKS: &[(&str, &str, &str, bool)] = &[
|
||||||
("Quotes", "#quotes", icons::SCROLL_TEXT),
|
("Dashboard", "/dashboard", icons::LAYOUT_DASHBOARD, false),
|
||||||
("Photos", "#photos", icons::FILE_IMAGE),
|
("Quotes", "#quotes", icons::SCROLL_TEXT, false),
|
||||||
("Persons", "#persons", icons::CONTACT),
|
("Photos", "#photos", icons::FILE_IMAGE, false),
|
||||||
("Tags", "#tags", icons::TAG),
|
("Persons", "#persons", icons::CONTACT, false),
|
||||||
("Users", "#users", icons::USERS),
|
("Tags", "#tags", icons::TAG, false),
|
||||||
("Logs", "#logs", icons::CLIPBOARD_CLOCK),
|
("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!(
|
html!(
|
||||||
div class="flex items-center text-sm gap-4 border-b border-neutral-200/25 bg-neutral-200/5 px-4 py-2" {
|
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"}
|
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="w-px h-5 bg-neutral-200/15 hidden sm:block" {}
|
||||||
div class="flex flex-row" {
|
div class="flex flex-row" {
|
||||||
@for link in LINKS {
|
@for link in LINKS {
|
||||||
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 !link.3 || user.is_some() {
|
||||||
@if uri.starts_with(link.1) {
|
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" {
|
||||||
div class="scale-[.75] text-neutral-300" {(PreEscaped(link.2))}
|
@if uri.starts_with(link.1) {
|
||||||
span class="text-neutral-200 font-light hidden lg:block" { (link.0) }
|
div class="scale-[.75] text-neutral-300" {(PreEscaped(link.2))}
|
||||||
} @else {
|
span class="text-neutral-200 font-light hidden lg:block" { (link.0) }
|
||||||
div class="scale-[.75] text-neutral-500" {(PreEscaped(link.2))}
|
} @else {
|
||||||
span class="text-neutral-400 font-light hidden lg:block" { (link.0) }
|
div class="scale-[.75] text-neutral-500" {(PreEscaped(link.2))}
|
||||||
|
span class="text-neutral-400 font-light hidden lg:block" { (link.0) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/web/components/user_miniprofile.rs
Normal file
33
src/web/components/user_miniprofile.rs
Normal 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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
1
src/web/icons/calendar-1.svg
Normal file
1
src/web/icons/calendar-1.svg
Normal 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 |
@@ -1,10 +1,13 @@
|
|||||||
// Below icons sourced from https://lucide.dev
|
// Below icons sourced from https://lucide.dev
|
||||||
pub const ARROW_RIGHT: &str = include_str!("arrow-right.svg");
|
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 CLIPBOARD_CLOCK: &str = include_str!("clipboard-clock.svg");
|
||||||
pub const CONTACT: &str = include_str!("contact.svg");
|
pub const CONTACT: &str = include_str!("contact.svg");
|
||||||
pub const FILE_IMAGE: &str = include_str!("file-image.svg");
|
pub const FILE_IMAGE: &str = include_str!("file-image.svg");
|
||||||
pub const LAYOUT_DASHBOARD: &str = include_str!("layout-dashboard.svg");
|
pub const LAYOUT_DASHBOARD: &str = include_str!("layout-dashboard.svg");
|
||||||
pub const SCROLL_TEXT: &str = include_str!("scroll-text.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 TAG: &str = include_str!("tag.svg");
|
||||||
pub const USER: &str = include_str!("user.svg");
|
pub const USER: &str = include_str!("user.svg");
|
||||||
pub const USER_KEY: &str = include_str!("user-key.svg");
|
pub const USER_KEY: &str = include_str!("user-key.svg");
|
||||||
|
|||||||
1
src/web/icons/server.svg
Normal file
1
src/web/icons/server.svg
Normal 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 |
1
src/web/icons/shield-user.svg
Normal file
1
src/web/icons/shield-user.svg
Normal 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 |
@@ -11,7 +11,7 @@ pub async fn page(req: Request) -> Markup {
|
|||||||
base(
|
base(
|
||||||
"Dashboard | Mnemosyne",
|
"Dashboard | Mnemosyne",
|
||||||
html!(
|
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"}
|
div class="text-6xl sm:text-8xl text-neutral-800/25 mt-16 text-center font-semibold font-lora select-none overflow-hidden" {"Mnemosyne"}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ use maud::{DOCTYPE, Markup, html};
|
|||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
pub fn pages() -> Router {
|
pub fn pages() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(index::page))
|
.route("/", get(index::page))
|
||||||
.route("/login", get(login::page))
|
.route("/login", get(login::page))
|
||||||
.route("/dashboard", get(dashboard::page))
|
.route("/dashboard", get(dashboard::page))
|
||||||
|
.route("/users", get(users::page))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn base(title: &str, inner: Markup) -> Markup {
|
pub fn base(title: &str, inner: Markup) -> Markup {
|
||||||
|
|||||||
58
src/web/pages/users.rs
Normal file
58
src/web/pages/users.rs
Normal 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
Reference in New Issue
Block a user