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",
|
"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]]
|
[[package]]
|
||||||
name = "base32"
|
name = "base32"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -934,6 +959,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
"base32",
|
"base32",
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -1289,6 +1315,19 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.149"
|
version = "1.0.149"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
axum = "0.8.8"
|
axum = "0.8.8"
|
||||||
|
axum-extra = { version = "0.12.5", features = ["form"] }
|
||||||
base32 = "0.5.1"
|
base32 = "0.5.1"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
chrono = { version = "0.4.43", features = ["serde"] }
|
||||||
|
|||||||
@@ -157,6 +157,20 @@ impl Name {
|
|||||||
None => Err(PersonError::NoNameWithId(id)),
|
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> {
|
pub fn set_primary(&mut self, conn: &Connection) -> Result<(), PersonError> {
|
||||||
if self.is_primary {
|
if self.is_primary {
|
||||||
return Err(PersonError::AlreadyPrimary);
|
return Err(PersonError::AlreadyPrimary);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use axum::{http::StatusCode, response::IntoResponse};
|
use axum::{http::StatusCode, response::IntoResponse};
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset, Utc};
|
||||||
use rusqlite::{Connection, OptionalExtension};
|
use rusqlite::{Connection, OptionalExtension};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -34,6 +34,15 @@ pub enum QuoteError {
|
|||||||
DatabaseError(#[from] DatabaseError),
|
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 {
|
impl Quote {
|
||||||
pub fn total_count(conn: &Connection) -> Result<i64, QuoteError> {
|
pub fn total_count(conn: &Connection) -> Result<i64, QuoteError> {
|
||||||
Ok(conn.query_row("SELECT COUNT(*) FROM quotes", (), |r| r.get(0))?)
|
Ok(conn.query_row("SELECT COUNT(*) FROM quotes", (), |r| r.get(0))?)
|
||||||
@@ -92,6 +101,18 @@ impl Quote {
|
|||||||
public,
|
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(
|
pub fn create(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
lines: Vec<(String, Name)>,
|
lines: Vec<(String, Name)>,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::{users::User, web::icons};
|
|||||||
// (SHOWTEXT, LINK, ICON, REQUIRES_LOG_IN)
|
// (SHOWTEXT, LINK, ICON, REQUIRES_LOG_IN)
|
||||||
const LINKS: &[(&str, &str, &str, bool)] = &[
|
const LINKS: &[(&str, &str, &str, bool)] = &[
|
||||||
("Dashboard", "/dashboard", icons::LAYOUT_DASHBOARD, false),
|
("Dashboard", "/dashboard", icons::LAYOUT_DASHBOARD, false),
|
||||||
("Quotes", "#quotes", icons::SCROLL_TEXT, false),
|
("Quotes", "/quotes", icons::SCROLL_TEXT, false),
|
||||||
("Photos", "#photos", icons::FILE_IMAGE, false),
|
("Photos", "#photos", icons::FILE_IMAGE, false),
|
||||||
("Persons", "/persons", icons::CONTACT, false),
|
("Persons", "/persons", icons::CONTACT, false),
|
||||||
("Tags", "/tags", icons::TAG, 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" {
|
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 {
|
@if let Some(loc) = "e.location {
|
||||||
span class="ml-3 scale-[.5]"{(PreEscaped(icons::MAP_PIN))} p { (loc) }
|
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 LOG_OUT: &str = include_str!("log-out.svg");
|
||||||
pub const MAP_PIN: &str = include_str!("map-pin.svg");
|
pub const MAP_PIN: &str = include_str!("map-pin.svg");
|
||||||
pub const PEN: &str = include_str!("pen.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 QUOTE: &str = include_str!("quote.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 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> {
|
pub async fn page(req: Request) -> Result<Markup, CompositeError> {
|
||||||
let u = User::authenticate(req.headers()).ok().flatten();
|
let u = User::authenticate(req.headers()).ok().flatten();
|
||||||
let mut conn = database::conn()?;
|
let conn = database::conn()?;
|
||||||
let tx = conn.transaction()?;
|
|
||||||
|
let newest_quote = Quote::get_newest(&conn)?;
|
||||||
|
|
||||||
Ok(base(
|
Ok(base(
|
||||||
"Dashboard | Mnemosyne",
|
"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="mx-auto max-w-4xl mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4" {
|
||||||
div class="flex flex-col" {
|
div class="flex flex-col" {
|
||||||
p {"Newest Quote"}
|
p {"Newest Quote"}
|
||||||
p class="text-neutral-500 font-light mb-4" {"This just in! This quote was added 15s ago."}
|
@if let Some(q) = newest_quote {
|
||||||
div class="flex-1 [&>div]:h-full" {(quote(&sample_quote_1()))}
|
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" {
|
div class="flex flex-col" {
|
||||||
p {"Quote of the Day"}
|
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" {
|
div class="mx-auto max-w-4xl mt-4 flex flex-row gap-2" {
|
||||||
(chip(html!({
|
(chip(html!({
|
||||||
@match Quote::total_count(&tx) {
|
@match Quote::total_count(&conn) {
|
||||||
Ok(count) => {(count) " QUOTES TOTAL"},
|
Ok(count) => {(count) " QUOTES TOTAL"},
|
||||||
Err(_) => span class="text-red-400" {"QUOTE COUNT ERR"},
|
Err(_) => span class="text-red-400" {"QUOTE COUNT ERR"},
|
||||||
}
|
}
|
||||||
})))
|
})))
|
||||||
(chip(html!({
|
(chip(html!({
|
||||||
@match Person::total_count(&tx) {
|
@match Person::total_count(&conn) {
|
||||||
Ok(count) => {(count) " PERSONS TOTAL"},
|
Ok(count) => {(count) " PERSONS TOTAL"},
|
||||||
Err(_) => span class="text-red-400" {"PERSON COUNT ERR"},
|
Err(_) => span class="text-red-400" {"PERSON COUNT ERR"},
|
||||||
}
|
}
|
||||||
})))
|
})))
|
||||||
(chip(html!({
|
(chip(html!({
|
||||||
@match Tag::total_count(&tx) {
|
@match Tag::total_count(&conn) {
|
||||||
Ok(count) => {(count) " TAGS TOTAL"},
|
Ok(count) => {(count) " TAGS TOTAL"},
|
||||||
Err(_) => span class="text-red-400" {"TAG COUNT ERR"}
|
Err(_) => span class="text-red-400" {"TAG COUNT ERR"}
|
||||||
}
|
}
|
||||||
})))
|
})))
|
||||||
(chip(html!({
|
(chip(html!({
|
||||||
@match User::total_count(&tx) {
|
@match User::total_count(&conn) {
|
||||||
Ok(count) => {(count) " USERS TOTAL"},
|
Ok(count) => {(count) " USERS TOTAL"},
|
||||||
Err(_) => span class="text-red-400" {"USER COUNT ERR"}
|
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 {
|
fn sample_quote_2() -> Quote {
|
||||||
Quote {
|
Quote {
|
||||||
id: Uuid::now_v7(),
|
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)?;
|
let logs = LogEntry::get_all(&tx)?;
|
||||||
|
|
||||||
Ok(base(
|
Ok(base(
|
||||||
"Persons | Mnemosyne",
|
"Logs | Mnemosyne",
|
||||||
html!(
|
html!(
|
||||||
(nav(Some(&u), req.uri().path()))
|
(nav(Some(&u), req.uri().path()))
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub mod index;
|
|||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod persons;
|
pub mod persons;
|
||||||
|
pub mod quotes;
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
pub mod usersettings;
|
pub mod usersettings;
|
||||||
@@ -30,6 +31,10 @@ pub fn pages() -> Router {
|
|||||||
.route("/persons", get(persons::page))
|
.route("/persons", get(persons::page))
|
||||||
.route("/persons/create", post(persons::create))
|
.route("/persons/create", post(persons::create))
|
||||||
.route("/logs", get(logs::page))
|
.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 {
|
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()?;
|
let conn = database::conn()?;
|
||||||
|
|
||||||
Ok(base(
|
Ok(base(
|
||||||
"Users | Mnemosyne",
|
"Create User | Mnemosyne",
|
||||||
html!(
|
html!(
|
||||||
(nav(u.as_ref(), req.uri().path()))
|
(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())?;
|
let u = User::authenticate(req.headers())?;
|
||||||
|
|
||||||
Ok(base(
|
Ok(base(
|
||||||
"Persons | Mnemosyne",
|
"User Settings | Mnemosyne",
|
||||||
html!(
|
html!(
|
||||||
(nav(u.as_ref(), req.uri().path()))
|
(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