You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
274 lines
11 KiB
Rust
274 lines
11 KiB
Rust
//! Submit page — new quote submission form.
|
|
|
|
use crate::api::{self, ApiError};
|
|
use crate::components::error::ErrorDisplay;
|
|
use crate::storage;
|
|
use crate::Route;
|
|
use quotesdb::CreateQuoteInput;
|
|
use wasm_bindgen_futures::spawn_local;
|
|
use web_sys::HtmlInputElement;
|
|
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
|
|
/// Submit page component.
|
|
///
|
|
/// Provides a form for creating a new quote with fields for text, author,
|
|
/// source, date, tags, and an optional custom auth code. On success, displays
|
|
/// the returned auth code prominently so the user can save it, and stores it
|
|
/// in session storage for immediate use.
|
|
#[function_component(SubmitPage)]
|
|
pub fn submit_page() -> Html {
|
|
let text = use_state(String::new);
|
|
let author = use_state(String::new);
|
|
let source = use_state(String::new);
|
|
let date = use_state(String::new);
|
|
let tags = use_state(String::new);
|
|
let custom_auth = use_state(String::new);
|
|
let submitting = use_state(|| false);
|
|
let error: UseStateHandle<Option<String>> = use_state(|| None);
|
|
let success: UseStateHandle<Option<(String, String)>> = use_state(|| None); // (quote_id, auth_code)
|
|
|
|
let onsubmit = {
|
|
let text = text.clone();
|
|
let author = author.clone();
|
|
let source = source.clone();
|
|
let date = date.clone();
|
|
let tags = tags.clone();
|
|
let custom_auth = custom_auth.clone();
|
|
let submitting = submitting.clone();
|
|
let error = error.clone();
|
|
let success = success.clone();
|
|
Callback::from(move |e: SubmitEvent| {
|
|
e.prevent_default();
|
|
if *submitting {
|
|
return;
|
|
}
|
|
let text_val = (*text).clone();
|
|
let author_val = (*author).clone();
|
|
let source_val = (*source).clone();
|
|
let date_val = (*date).clone();
|
|
let tags_val = (*tags).clone();
|
|
let auth_val = (*custom_auth).clone();
|
|
|
|
if text_val.is_empty() {
|
|
error.set(Some("Quote text is required.".to_string()));
|
|
return;
|
|
}
|
|
|
|
let parsed_tags: Vec<String> = tags_val
|
|
.split(',')
|
|
.map(|t| t.trim().to_string())
|
|
.filter(|t| !t.is_empty())
|
|
.collect();
|
|
|
|
let input = CreateQuoteInput {
|
|
text: text_val,
|
|
author: if author_val.is_empty() {
|
|
"Anonymous".to_string()
|
|
} else {
|
|
author_val
|
|
},
|
|
source: if source_val.is_empty() {
|
|
None
|
|
} else {
|
|
Some(source_val)
|
|
},
|
|
date: if date_val.is_empty() {
|
|
None
|
|
} else {
|
|
Some(date_val)
|
|
},
|
|
tags: parsed_tags,
|
|
auth_code: if auth_val.is_empty() {
|
|
None
|
|
} else {
|
|
Some(auth_val)
|
|
},
|
|
};
|
|
|
|
submitting.set(true);
|
|
error.set(None);
|
|
|
|
let submitting = submitting.clone();
|
|
let error = error.clone();
|
|
let success = success.clone();
|
|
spawn_local(async move {
|
|
match api::create_quote(&input).await {
|
|
Ok(resp) => {
|
|
storage::set_auth_code(&resp.quote.id, &resp.auth_code);
|
|
success.set(Some((resp.quote.id, resp.auth_code)));
|
|
submitting.set(false);
|
|
}
|
|
Err(ApiError::Server { status, message }) => {
|
|
error.set(Some(format!("Error {status}: {message}")));
|
|
submitting.set(false);
|
|
}
|
|
Err(e) => {
|
|
error.set(Some(e.to_string()));
|
|
submitting.set(false);
|
|
}
|
|
}
|
|
});
|
|
})
|
|
};
|
|
|
|
if let Some((quote_id, auth_code)) = (*success).clone() {
|
|
return html! {
|
|
<div class="page-submit page-submit--success">
|
|
<h1 class="page-submit__title">{ "Quote Submitted!" }</h1>
|
|
<p>{ "Your quote has been added. Save your auth code below — you'll need it to edit or delete this quote." }</p>
|
|
<div class="page-submit__auth-code-box">
|
|
<strong>{ "Auth Code: " }</strong>
|
|
<code class="page-submit__auth-code">{ &auth_code }</code>
|
|
</div>
|
|
<div class="page-submit__actions">
|
|
<Link<Route>
|
|
to={Route::QuoteDetail { id: quote_id.clone() }}
|
|
classes="btn btn--primary"
|
|
>
|
|
{ "View your quote" }
|
|
</Link<Route>>
|
|
<Link<Route> to={Route::Browse} classes="btn">
|
|
{ "Browse all quotes" }
|
|
</Link<Route>>
|
|
</div>
|
|
</div>
|
|
};
|
|
}
|
|
|
|
html! {
|
|
<div class="page-submit">
|
|
<h1 class="page-submit__title">{ "Submit a Quote" }</h1>
|
|
|
|
if let Some(err) = (*error).clone() {
|
|
<ErrorDisplay message={err} />
|
|
}
|
|
|
|
<form class="submit-form" onsubmit={onsubmit}>
|
|
<div class="submit-form__field">
|
|
<label class="submit-form__label" for="text">{ "Quote text *" }</label>
|
|
<textarea
|
|
id="text"
|
|
class="submit-form__textarea"
|
|
placeholder="Enter the quote text..."
|
|
value={(*text).clone()}
|
|
oninput={{
|
|
let text = text.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let el: web_sys::HtmlTextAreaElement = e.target_unchecked_into();
|
|
text.set(el.value());
|
|
})
|
|
}}
|
|
required=true
|
|
/>
|
|
</div>
|
|
|
|
<div class="submit-form__field">
|
|
<label class="submit-form__label" for="author">{ "Author (optional, defaults to Anonymous)" }</label>
|
|
<input
|
|
id="author"
|
|
class="submit-form__input"
|
|
type="text"
|
|
placeholder="e.g. Mark Twain"
|
|
value={(*author).clone()}
|
|
oninput={{
|
|
let author = author.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let input: HtmlInputElement = e.target_unchecked_into();
|
|
author.set(input.value());
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="submit-form__field">
|
|
<label class="submit-form__label" for="source">{ "Source (optional)" }</label>
|
|
<input
|
|
id="source"
|
|
class="submit-form__input"
|
|
type="text"
|
|
placeholder="e.g. Adventures of Huckleberry Finn"
|
|
value={(*source).clone()}
|
|
oninput={{
|
|
let source = source.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let input: HtmlInputElement = e.target_unchecked_into();
|
|
source.set(input.value());
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="submit-form__field">
|
|
<label class="submit-form__label" for="date">{ "Date (optional)" }</label>
|
|
<input
|
|
id="date"
|
|
class="submit-form__input"
|
|
type="date"
|
|
value={(*date).clone()}
|
|
oninput={{
|
|
let date = date.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let input: HtmlInputElement = e.target_unchecked_into();
|
|
date.set(input.value());
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="submit-form__field">
|
|
<label class="submit-form__label" for="tags">{ "Tags (optional, comma-separated)" }</label>
|
|
<input
|
|
id="tags"
|
|
class="submit-form__input"
|
|
type="text"
|
|
placeholder="e.g. humor, classic, american"
|
|
value={(*tags).clone()}
|
|
oninput={{
|
|
let tags = tags.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let input: HtmlInputElement = e.target_unchecked_into();
|
|
tags.set(input.value());
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="submit-form__field">
|
|
<label class="submit-form__label" for="auth_code">
|
|
{ "Edit/delete passphrase (auto-generated if left blank)" }
|
|
</label>
|
|
<input
|
|
id="auth_code"
|
|
class="submit-form__input"
|
|
type="text"
|
|
placeholder="word-word-word-word (auto-generated if empty)"
|
|
value={(*custom_auth).clone()}
|
|
oninput={{
|
|
let custom_auth = custom_auth.clone();
|
|
Callback::from(move |e: InputEvent| {
|
|
let input: HtmlInputElement = e.target_unchecked_into();
|
|
custom_auth.set(input.value());
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="submit-form__actions">
|
|
<button
|
|
type="submit"
|
|
class="btn btn--primary"
|
|
disabled={*submitting}
|
|
>
|
|
if *submitting {
|
|
{ "Submitting..." }
|
|
} else {
|
|
{ "Submit Quote" }
|
|
}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
}
|
|
}
|