merge upstream
All checks were successful
mnemo-build-and-publish / gractwo-mnemo-build (push) Successful in 34s
All checks were successful
mnemo-build-and-publish / gractwo-mnemo-build (push) Successful in 34s
This commit is contained in:
39
Cargo.lock
generated
39
Cargo.lock
generated
@@ -185,6 +185,31 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-extra"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"serde_html_form",
|
||||
"serde_path_to_error",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base32"
|
||||
version = "0.5.1"
|
||||
@@ -934,6 +959,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"base32",
|
||||
"base64",
|
||||
"chrono",
|
||||
@@ -1289,6 +1315,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_html_form"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
|
||||
@@ -6,6 +6,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
argon2 = "0.5.3"
|
||||
axum = "0.8.8"
|
||||
axum-extra = { version = "0.12.5", features = ["form"] }
|
||||
base32 = "0.5.1"
|
||||
base64 = "0.22.1"
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
|
||||
@@ -157,6 +157,20 @@ impl Name {
|
||||
None => Err(PersonError::NoNameWithId(id)),
|
||||
}
|
||||
}
|
||||
pub fn get_all(conn: &Connection) -> Result<Vec<Name>, PersonError> {
|
||||
Ok(conn
|
||||
.prepare("SELECT id, is_primary, person_id, created_by, name FROM names")?
|
||||
.query_map((), |r| {
|
||||
Ok(Name {
|
||||
id: r.get(0)?,
|
||||
is_primary: r.get(1)?,
|
||||
person_id: r.get(2)?,
|
||||
created_by: r.get(3)?,
|
||||
name: r.get(4)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<Name>, _>>()?)
|
||||
}
|
||||
pub fn set_primary(&mut self, conn: &Connection) -> Result<(), PersonError> {
|
||||
if self.is_primary {
|
||||
return Err(PersonError::AlreadyPrimary);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use rusqlite::{Connection, OptionalExtension};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
@@ -34,6 +34,15 @@ pub enum QuoteError {
|
||||
DatabaseError(#[from] DatabaseError),
|
||||
}
|
||||
|
||||
impl Quote {
|
||||
pub fn get_creation_timestamp(&self) -> DateTime<Utc> {
|
||||
// unwrap here because all IDs use UUIDv7
|
||||
let (s, n) = self.id.get_timestamp().unwrap().to_unix();
|
||||
// unwrap here because timestamps held by UUIDs are valid by spec
|
||||
DateTime::from_timestamp(s as i64, n).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Quote {
|
||||
pub fn total_count(conn: &Connection) -> Result<i64, QuoteError> {
|
||||
Ok(conn.query_row("SELECT COUNT(*) FROM quotes", (), |r| r.get(0))?)
|
||||
@@ -92,6 +101,18 @@ impl Quote {
|
||||
public,
|
||||
})
|
||||
}
|
||||
pub fn get_newest(conn: &Connection) -> Result<Option<Quote>, QuoteError> {
|
||||
let id: Option<Uuid> = conn
|
||||
.query_row("SELECT id FROM quotes ORDER BY id DESC LIMIT 1", (), |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.optional()?;
|
||||
|
||||
match id {
|
||||
Some(id) => Ok(Some(Self::get_by_id(conn, id)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
pub fn create(
|
||||
conn: &Connection,
|
||||
lines: Vec<(String, Name)>,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{users::User, web::icons};
|
||||
// (SHOWTEXT, LINK, ICON, REQUIRES_LOG_IN)
|
||||
const LINKS: &[(&str, &str, &str, bool)] = &[
|
||||
("Dashboard", "/dashboard", icons::LAYOUT_DASHBOARD, false),
|
||||
("Quotes", "#quotes", icons::SCROLL_TEXT, false),
|
||||
("Quotes", "/quotes", icons::SCROLL_TEXT, false),
|
||||
("Photos", "#photos", icons::FILE_IMAGE, false),
|
||||
("Persons", "/persons", icons::CONTACT, false),
|
||||
("Tags", "/tags", icons::TAG, false),
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn quote(quote: &Quote) -> Markup {
|
||||
}
|
||||
}
|
||||
div class="flex flex-row text-neutral-400 mt-auto pt-4 items-center font-light text-xs" {
|
||||
p {(quote.timestamp.format("%d/%m/%Y %H:%M"))}
|
||||
p {(quote.timestamp.format("%Y-%m-%d %H:%M"))}
|
||||
@if let Some(loc) = "e.location {
|
||||
span class="ml-3 scale-[.5]"{(PreEscaped(icons::MAP_PIN))} p { (loc) }
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ pub const LAYOUT_DASHBOARD: &str = include_str!("layout-dashboard.svg");
|
||||
pub const LOG_OUT: &str = include_str!("log-out.svg");
|
||||
pub const MAP_PIN: &str = include_str!("map-pin.svg");
|
||||
pub const PEN: &str = include_str!("pen.svg");
|
||||
pub const PLUS: &str = include_str!("plus.svg");
|
||||
pub const QUOTE: &str = include_str!("quote.svg");
|
||||
pub const SCROLL_TEXT: &str = include_str!("scroll-text.svg");
|
||||
pub const SERVER: &str = include_str!("server.svg");
|
||||
|
||||
1
src/web/icons/plus.svg
Normal file
1
src/web/icons/plus.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-plus-icon lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||
|
After Width: | Height: | Size: 272 B |
@@ -24,8 +24,9 @@ const LINKS: &[(&str, &str, &str)] = &[
|
||||
|
||||
pub async fn page(req: Request) -> Result<Markup, CompositeError> {
|
||||
let u = User::authenticate(req.headers()).ok().flatten();
|
||||
let mut conn = database::conn()?;
|
||||
let tx = conn.transaction()?;
|
||||
let conn = database::conn()?;
|
||||
|
||||
let newest_quote = Quote::get_newest(&conn)?;
|
||||
|
||||
Ok(base(
|
||||
"Dashboard | Mnemosyne",
|
||||
@@ -35,8 +36,15 @@ pub async fn page(req: Request) -> Result<Markup, CompositeError> {
|
||||
div class="mx-auto max-w-4xl mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4" {
|
||||
div class="flex flex-col" {
|
||||
p {"Newest Quote"}
|
||||
p class="text-neutral-500 font-light mb-4" {"This just in! This quote was added 15s ago."}
|
||||
div class="flex-1 [&>div]:h-full" {(quote(&sample_quote_1()))}
|
||||
@if let Some(q) = newest_quote {
|
||||
p class="text-neutral-500 font-light mb-4" {
|
||||
"This just in! This quote was added "
|
||||
(format_time_ago(q.get_creation_timestamp())) " ago."
|
||||
}
|
||||
div class="flex-1 [&>div]:h-full" {(quote(&q))}
|
||||
} @else {
|
||||
p class="text-neutral-500 font-light mb-4" {"No quotes yet."}
|
||||
}
|
||||
}
|
||||
div class="flex flex-col" {
|
||||
p {"Quote of the Day"}
|
||||
@@ -60,25 +68,25 @@ pub async fn page(req: Request) -> Result<Markup, CompositeError> {
|
||||
}
|
||||
div class="mx-auto max-w-4xl mt-4 flex flex-row gap-2" {
|
||||
(chip(html!({
|
||||
@match Quote::total_count(&tx) {
|
||||
@match Quote::total_count(&conn) {
|
||||
Ok(count) => {(count) " QUOTES TOTAL"},
|
||||
Err(_) => span class="text-red-400" {"QUOTE COUNT ERR"},
|
||||
}
|
||||
})))
|
||||
(chip(html!({
|
||||
@match Person::total_count(&tx) {
|
||||
@match Person::total_count(&conn) {
|
||||
Ok(count) => {(count) " PERSONS TOTAL"},
|
||||
Err(_) => span class="text-red-400" {"PERSON COUNT ERR"},
|
||||
}
|
||||
})))
|
||||
(chip(html!({
|
||||
@match Tag::total_count(&tx) {
|
||||
@match Tag::total_count(&conn) {
|
||||
Ok(count) => {(count) " TAGS TOTAL"},
|
||||
Err(_) => span class="text-red-400" {"TAG COUNT ERR"}
|
||||
}
|
||||
})))
|
||||
(chip(html!({
|
||||
@match User::total_count(&tx) {
|
||||
@match User::total_count(&conn) {
|
||||
Ok(count) => {(count) " USERS TOTAL"},
|
||||
Err(_) => span class="text-red-400" {"USER COUNT ERR"}
|
||||
}
|
||||
@@ -90,41 +98,6 @@ pub async fn page(req: Request) -> Result<Markup, CompositeError> {
|
||||
))
|
||||
}
|
||||
|
||||
fn sample_quote_1() -> Quote {
|
||||
Quote {
|
||||
id: Uuid::now_v7(),
|
||||
public: true,
|
||||
location: Some(String::from("Poznań")),
|
||||
context: Some(String::from("Wykład z językoznawstwa")),
|
||||
created_by: Uuid::max(),
|
||||
timestamp: DateTime::from(Utc::now()),
|
||||
lines: vec![
|
||||
QuoteLine {
|
||||
id: Uuid::now_v7(),
|
||||
content: String::from("Nie wiem, czy są tutaj osoby fanowskie zipline-ów?"),
|
||||
attribution: Name {
|
||||
id: Uuid::nil(),
|
||||
created_by: Uuid::max(),
|
||||
person_id: Uuid::now_v7(),
|
||||
is_primary: true,
|
||||
name: String::from("dr. Barbara Konat"),
|
||||
},
|
||||
},
|
||||
QuoteLine {
|
||||
id: Uuid::now_v7(),
|
||||
content: String::from("Taka uprząż co robi pziuuum!"),
|
||||
attribution: Name {
|
||||
id: Uuid::nil(),
|
||||
created_by: Uuid::max(),
|
||||
person_id: Uuid::now_v7(),
|
||||
is_primary: true,
|
||||
name: String::from("dr. Barbara Konat"),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_quote_2() -> Quote {
|
||||
Quote {
|
||||
id: Uuid::now_v7(),
|
||||
@@ -159,3 +132,13 @@ fn sample_quote_2() -> Quote {
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn format_time_ago(dt: DateTime<Utc>) -> String {
|
||||
let secs = Utc::now().signed_duration_since(dt).num_seconds();
|
||||
match secs {
|
||||
..60 => format!("{}s", secs),
|
||||
60..3600 => format!("{}m", secs / 60),
|
||||
3600..86400 => format!("{}h", secs / 3600),
|
||||
_ => format!("{}d", secs / 86400),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn page(req: Request) -> Result<Response, CompositeError> {
|
||||
let logs = LogEntry::get_all(&tx)?;
|
||||
|
||||
Ok(base(
|
||||
"Persons | Mnemosyne",
|
||||
"Logs | Mnemosyne",
|
||||
html!(
|
||||
(nav(Some(&u), req.uri().path()))
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod index;
|
||||
pub mod login;
|
||||
pub mod logs;
|
||||
pub mod persons;
|
||||
pub mod quotes;
|
||||
pub mod tags;
|
||||
pub mod users;
|
||||
pub mod usersettings;
|
||||
@@ -30,6 +31,10 @@ pub fn pages() -> Router {
|
||||
.route("/persons", get(persons::page))
|
||||
.route("/persons/create", post(persons::create))
|
||||
.route("/logs", get(logs::page))
|
||||
//
|
||||
.route("/quotes", get(quotes::page))
|
||||
.route("/quotes/add", get(quotes::add::page))
|
||||
.route("/quotes/add-form", post(quotes::add::form))
|
||||
}
|
||||
|
||||
pub fn base(title: &str, inner: Markup) -> Markup {
|
||||
|
||||
43
src/web/pages/quotes.rs
Normal file
43
src/web/pages/quotes.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use axum::{
|
||||
extract::Request,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use maud::{PreEscaped, html};
|
||||
|
||||
use crate::{
|
||||
error::CompositeError,
|
||||
users::{User, auth::UserAuthenticate},
|
||||
web::{components::nav::nav, icons, pages::base},
|
||||
};
|
||||
|
||||
pub mod add;
|
||||
|
||||
pub async fn page(req: Request) -> Result<Response, CompositeError> {
|
||||
let u = User::authenticate(req.headers())?;
|
||||
|
||||
Ok(base(
|
||||
"Quotes | Mnemosyne",
|
||||
html!(
|
||||
(nav(u.as_ref(), req.uri().path()))
|
||||
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
div class="my-4 flex justify-between" {
|
||||
p class="flex items-center gap-2" {
|
||||
span class="text-neutral-500" {(PreEscaped(icons::SCROLL_TEXT))}
|
||||
span class="text-2xl font-semibold font-lora" {"Quotes"}
|
||||
}
|
||||
@if let Some(_) = u {
|
||||
a href="/quotes/add" class="group border rounded flex items-center gap-1 px-2 border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-400/5 hover:bg-neutral-400/10" {
|
||||
span class="text-neutral-300 group-hover:text-neutral-200" {(PreEscaped(icons::PLUS))}
|
||||
span class="text-neutral-300 group-hover:text-neutral-200" {"Add quote"}
|
||||
}
|
||||
}
|
||||
}
|
||||
input class="border w-full border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-950/50 p-2 rounded"
|
||||
placeholder="Search for quotes...";
|
||||
div class="text-center p-4" {"Search not yet implemented."}
|
||||
}
|
||||
),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
153
src/web/pages/quotes/add.rs
Normal file
153
src/web/pages/quotes/add.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::Request,
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::Form;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use chrono_tz::Europe::Warsaw;
|
||||
use maud::{PreEscaped, html};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
database,
|
||||
error::CompositeError,
|
||||
logs::{LogAction, LogEntry},
|
||||
persons::Name,
|
||||
quotes::Quote,
|
||||
users::{
|
||||
User,
|
||||
auth::{UserAuthRequired, UserAuthenticate},
|
||||
},
|
||||
web::{components::nav::nav, icons, pages::base},
|
||||
};
|
||||
|
||||
pub async fn page(req: Request) -> Result<Response, CompositeError> {
|
||||
let u = User::authenticate(req.headers())?;
|
||||
let conn = database::conn()?;
|
||||
let names = Name::get_all(&conn)?;
|
||||
|
||||
Ok(base(
|
||||
"Add Quote | Mnemosyne",
|
||||
html!(
|
||||
(nav(u.as_ref(), req.uri().path()))
|
||||
|
||||
div class="max-w-4xl mx-auto px-2" {
|
||||
div class="my-4 flex justify-between" {
|
||||
p class="flex items-center gap-2" {
|
||||
span class="text-neutral-500" {(PreEscaped(icons::SCROLL_TEXT))}
|
||||
span class="text-2xl font-semibold font-lora" {"Quote Maker"}
|
||||
}
|
||||
}
|
||||
form method="post" action="/quotes/add-form"
|
||||
class="border border-neutral-200/25 bg-neutral-200/5 rounded-md p-4 flex flex-col" {
|
||||
@for i in 1..=2 {
|
||||
div class="flex justify-between gap-4" {
|
||||
div class="flex flex-col flex-1" {
|
||||
label class="w-full" {
|
||||
p class="mb-1" {(format!("Quote Line #{i}"))}
|
||||
input type="text" name="quoteline" placeholder="They said..." autocomplete="off"
|
||||
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
||||
}
|
||||
}
|
||||
div class="flex flex-col" {
|
||||
label {
|
||||
p class="mb-1" {(format!("Quote Author #{i}"))}
|
||||
select name="quoteauthor" autocomplete="off"
|
||||
class="px-2 py-1.5 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25"{
|
||||
option {"--"}
|
||||
@for name in &names {
|
||||
option value=(name.id.to_string()) {(name.name)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hr class="border-neutral-200/25 my-4";
|
||||
div class="flex gap-4 justify-between" {
|
||||
div class="flex flex-col flex-1" {
|
||||
label class="w-full"{
|
||||
p class="mb-1" {"Location"}
|
||||
input type="text" name="location" autocomplete="off" placeholder="Right there!"
|
||||
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
||||
}
|
||||
}
|
||||
div class="flex flex-col flex-1" {
|
||||
label class="w-full" {
|
||||
p class="mb-1" {"Time of utterance"}
|
||||
input type="hidden" name="tz_offset" id="tz_offset" value="0";
|
||||
script { (PreEscaped("document.getElementById('tz_offset').value = new Date().getTimezoneOffset();")) }
|
||||
input type="datetime-local" name="time" autocomplete="off"
|
||||
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="flex gap-4 justify-between" {
|
||||
div class="flex flex-col flex-1" {
|
||||
label class="w-full" {
|
||||
p class="mb-1" {"Context"}
|
||||
input type="text" name="context" autocomplete="off" placeholder="It was like this.."
|
||||
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
||||
}
|
||||
}
|
||||
button type="submit" class="border mt-auto mb-2 cursor-pointer rounded h-fit px-2 py-1 bg-neutral-200/5 border-neutral-200/25 hover:border-neutral-200/45 hover:bg-neutral-200/15" {
|
||||
"Submit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct IncomingQuote {
|
||||
#[serde(rename = "quoteline")]
|
||||
lines: Vec<String>,
|
||||
#[serde(rename = "quoteauthor")]
|
||||
authors: Vec<Uuid>,
|
||||
location: String,
|
||||
time: String,
|
||||
tz_offset: Option<i32>,
|
||||
context: String,
|
||||
}
|
||||
pub async fn form(
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<IncomingQuote>,
|
||||
) -> Result<Response, CompositeError> {
|
||||
let u = User::authenticate(&headers)?.required()?;
|
||||
let mut conn = database::conn()?;
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
let authors = form
|
||||
.authors
|
||||
.into_iter()
|
||||
.map(|nid| Name::get_by_id(&tx, nid).unwrap());
|
||||
let lines = form.lines.into_iter().zip(authors).collect();
|
||||
let offset = form
|
||||
.tz_offset
|
||||
.and_then(|mins| chrono::FixedOffset::west_opt(mins * 60))
|
||||
.unwrap_or_else(|| chrono::FixedOffset::west_opt(0).unwrap());
|
||||
|
||||
let timestamp = chrono::NaiveDateTime::parse_from_str(&form.time, "%Y-%m-%dT%H:%M")
|
||||
.map(|ndt| offset.from_local_datetime(&ndt).unwrap())
|
||||
.unwrap_or_else(|_| Utc::now().with_timezone(&Warsaw).fixed_offset());
|
||||
let context = match form.context.trim() {
|
||||
"" => None,
|
||||
s => Some(s.to_string()),
|
||||
};
|
||||
let location = match form.location.trim() {
|
||||
"" => None,
|
||||
s => Some(s.to_string()),
|
||||
};
|
||||
|
||||
let q = Quote::create(&tx, lines, timestamp, context, location, u.id, false)?;
|
||||
LogEntry::new(&tx, u, LogAction::CreateQuote { id: q.id })?;
|
||||
tx.commit()?;
|
||||
|
||||
Ok(Json(q).into_response())
|
||||
}
|
||||
@@ -25,7 +25,7 @@ pub async fn page(req: Request) -> Result<Response, AuthError> {
|
||||
let conn = database::conn()?;
|
||||
|
||||
Ok(base(
|
||||
"Users | Mnemosyne",
|
||||
"Create User | Mnemosyne",
|
||||
html!(
|
||||
(nav(u.as_ref(), req.uri().path()))
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ pub async fn page(req: Request) -> Result<Response, AuthError> {
|
||||
let u = User::authenticate(req.headers())?;
|
||||
|
||||
Ok(base(
|
||||
"Persons | Mnemosyne",
|
||||
"User Settings | Mnemosyne",
|
||||
html!(
|
||||
(nav(u.as_ref(), req.uri().path()))
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user