Files
mnemosyne/src/web/pages/quotes/add.rs

191 lines
7.6 KiB
Rust

use axum::{
extract::{Request, State},
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
};
use axum_extra::extract::Form;
use chrono::NaiveDateTime;
use maud::{Markup, PreEscaped, html};
use reqwest::Url;
use serde::Deserialize;
use uuid::Uuid;
use crate::{
MnemoState,
error::CompositeError,
logs::{LogAction, LogEntry},
persons::Name,
quotes::Quote,
users::{
User,
auth::{UserAuthRequired, UserAuthenticate},
},
web::{components::nav::nav, icons, pages::base},
};
const LINE_ADD_RM_SCRIPT: &str = include_str!("line-add-rm.js");
const PREFILL_TIME_SCRIPT: &str = include_str!("prefill-time.js");
pub async fn page(
State(state): State<MnemoState>,
req: Request,
) -> Result<Response, CompositeError> {
let mut conn = state.pool.acquire().await?;
let u = match User::authenticate(&mut *conn, req.headers()).await? {
Some(u) => u,
None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()),
};
let names = Name::get_all(&mut *conn).await?;
Ok(base(
"Add Quote | Mnemosyne",
html!(
(nav(Some(&u), 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" {
div quotelines class="flex flex-col" {
@for i in 1..=3 {(maker_line_row(i==1, &names))}
}
template quotelinetemplate {
(maker_line_row(false, &names))
}
div class="flex flex-row gap-2" {
hr class="border-neutral-200/25 flex-1 my-4";
button addlinebtn type="button"
class="w-fit text-neutral-400 hover:text-neutral-300 cursor-pointer" {
"Add line"
}
}
script {(PreEscaped(LINE_ADD_RM_SCRIPT))}
script {(PreEscaped(PREFILL_TIME_SCRIPT))}
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())
}
fn maker_line_row(rm_disabled: bool, names: &[Name]) -> Markup {
html!(
div quoteline class="flex gap-4" {
div class="flex flex-col flex-1" {
label class="w-full" {
p class="mb-1" {"Quote line"}
input type="text" name="quoteline" placeholder="They said..." autocomplete="off" required
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
}
}
div class="flex flex-col ml-auto" {
label {
p class="mb-1" {"Attribution"}
select name="quoteauthor" autocomplete="off" required
class="px-2 py-1.5 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25"{
option value="" {"--"}
@for name in names {
option value=(name.id.to_string()) {(name.name)}
}
}
}
}
button rmlinebtn disabled?[rm_disabled] type="button" class="h-fit mt-auto mb-2 p-1 bg-neutral-200/5 hover:bg-neutral-200/15 rounded border border-neutral-200/25 hover:border-neutral-200/45 cursor-pointer disabled:cursor-not-allowed disabled:opacity-[.5]" {
(PreEscaped(icons::CIRCLE_MINUS))
}
}
)
}
#[derive(Deserialize, Debug)]
pub struct IncomingQuote {
#[serde(rename = "quoteline")]
lines: Vec<String>,
#[serde(rename = "quoteauthor")]
authors: Vec<Uuid>,
location: String,
time: String,
context: String,
}
pub async fn form(
State(state): State<MnemoState>,
headers: HeaderMap,
Form(form): Form<IncomingQuote>,
) -> Result<Response, CompositeError> {
let mut tx = state.pool.begin().await?;
let u = User::authenticate(&mut *tx, &headers).await?.required()?;
let mut authors = Vec::new();
for nid in form.authors {
authors.push(Name::get_by_id(&mut *tx, nid).await.unwrap());
}
let lines = form
.lines
.into_iter()
.zip(authors)
.map(|(l, a)| (l, vec![a]))
.collect();
let timestamp = match NaiveDateTime::parse_from_str(&form.time, "%Y-%m-%dT%H:%M") {
Ok(ts) => ts,
Err(_) => return Ok("Time was formatted wrong.".into_response()),
};
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(&mut *tx, lines, timestamp, context, location, u.id, false).await?;
LogEntry::new(&mut *tx, u, LogAction::CreateQuote { id: q.id }).await?;
tx.commit().await?;
if let Ok(webhook_url) = std::env::var("DISCORD_WEBHOOK_URL") {
match Url::parse(&webhook_url) {
Ok(u) => q.post_msg_webhook(u),
Err(e) => log::error!("Tried to post webhook, failed to parse url: {e}"),
}
}
Ok(Redirect::to("/dashboard").into_response())
}