191 lines
7.6 KiB
Rust
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())
|
|
}
|