feat(quotesdb): implement all UI page components (Home, Browse, QuoteDetail, Author, Submit)

- HomePage: fetches random quote on mount, displays with QuoteCard and browse/submit links
- BrowsePage: paginated list with author and tag filter inputs, Pagination component
- QuotePage: view/edit/delete with AuthModal gating, 403/404 handling, sessionStorage auth
- AuthorPage: lists quotes by author with tag filter and pagination
- SubmitPage: full form with all fields, success state showing auth code prominently
- Tag filter (d3d502) integrated into Browse and Author pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quotesdb
Elijah Voigt 3 months ago
parent 52e771e9c4
commit 183994b3dc

@ -1,15 +1,116 @@
//! Author page — all quotes by a specific author.
use crate::api;
use crate::components::error::ErrorDisplay;
use crate::components::pagination::Pagination;
use crate::components::quote_card::QuoteCard;
use quotesdb::Quote;
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlInputElement;
use yew::prelude::*;
/// Props for the [`AuthorPage`] component.
#[derive(Properties, PartialEq)]
pub struct AuthorPageProps {
/// The author name from the route parameter.
/// The author name from the route parameter (URL-decoded by yew-router).
pub name: String,
}
/// Author page component. Lists all quotes attributed to the given author.
/// Author page component.
///
/// Lists all quotes attributed to the given author, with pagination support.
/// Also provides a tag filter to narrow results within the author's quotes.
#[function_component(AuthorPage)]
pub fn author_page(props: &AuthorPageProps) -> Html {
html! { <p>{ format!("Loading quotes by {}...", props.name) }</p> }
let name = props.name.clone();
let page = use_state(|| 1u32);
let total_pages = use_state(|| 1u32);
let tag_filter = use_state(String::new);
let quotes: UseStateHandle<Vec<Quote>> = use_state(Vec::new);
let error: UseStateHandle<Option<String>> = use_state(|| None);
let loading = use_state(|| true);
{
let name = name.clone();
let page = page.clone();
let total_pages = total_pages.clone();
let tag_filter = tag_filter.clone();
let quotes = quotes.clone();
let error = error.clone();
let loading = loading.clone();
let page_val = *page;
let tag_val = (*tag_filter).clone();
use_effect_with((name.clone(), page_val, tag_val.clone()), move |_| {
loading.set(true);
error.set(None);
let tag = if tag_val.is_empty() {
None
} else {
Some(tag_val.clone())
};
spawn_local(async move {
match api::list_quotes(page_val, Some(&name), tag.as_deref()).await {
Ok(resp) => {
quotes.set(resp.quotes);
total_pages.set(resp.total_pages.max(1));
loading.set(false);
}
Err(e) => {
error.set(Some(e.to_string()));
loading.set(false);
}
}
});
});
}
let on_page = {
let page = page.clone();
Callback::from(move |p: u32| page.set(p))
};
let on_tag_input = {
let tag_filter = tag_filter.clone();
let page = page.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
tag_filter.set(input.value());
page.set(1);
})
};
html! {
<div class="page-author">
<h1 class="page-author__title">{ format!("Quotes by {}", props.name) }</h1>
<div class="page-author__filters">
<input
class="page-author__filter-input"
type="text"
placeholder="Filter by tag..."
value={(*tag_filter).clone()}
oninput={on_tag_input}
/>
</div>
if *loading {
<p class="page-author__loading">{ "Loading..." }</p>
} else if let Some(err) = (*error).clone() {
<ErrorDisplay message={err} />
} else if quotes.is_empty() {
<p class="page-author__empty">{ "No quotes found for this author." }</p>
} else {
<div class="page-author__list">
{ for (*quotes).iter().map(|q| html! {
<QuoteCard quote={q.clone()} />
}) }
</div>
<Pagination
page={*page}
total_pages={*total_pages}
on_page={on_page}
/>
}
</div>
}
}

@ -1,8 +1,133 @@
//! Browse page — paginated quote list with author and tag filters.
//! Browse page — paginated quote list with author and tag filter controls.
use crate::api;
use crate::components::error::ErrorDisplay;
use crate::components::pagination::Pagination;
use crate::components::quote_card::QuoteCard;
use quotesdb::Quote;
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlInputElement;
use yew::prelude::*;
/// Browse page component. Displays paginated quotes with filter controls.
/// Browse page component.
///
/// Displays a paginated list of quotes. Supports filtering by author name
/// and tag. Fetches from the API whenever page, author, or tag state changes.
#[function_component(BrowsePage)]
pub fn browse_page() -> Html {
html! { <p>{ "Loading..." }</p> }
let page = use_state(|| 1u32);
let total_pages = use_state(|| 1u32);
let author_filter = use_state(String::new);
let tag_filter = use_state(String::new);
let quotes: UseStateHandle<Vec<Quote>> = use_state(Vec::new);
let error: UseStateHandle<Option<String>> = use_state(|| None);
let loading = use_state(|| true);
// Fetch quotes whenever page, author, or tag changes
{
let page = page.clone();
let total_pages = total_pages.clone();
let author_filter = author_filter.clone();
let tag_filter = tag_filter.clone();
let quotes = quotes.clone();
let error = error.clone();
let loading = loading.clone();
let page_val = *page;
let author_val = (*author_filter).clone();
let tag_val = (*tag_filter).clone();
use_effect_with((page_val, author_val.clone(), tag_val.clone()), move |_| {
loading.set(true);
error.set(None);
let author = if author_val.is_empty() {
None
} else {
Some(author_val.clone())
};
let tag = if tag_val.is_empty() {
None
} else {
Some(tag_val.clone())
};
spawn_local(async move {
match api::list_quotes(page_val, author.as_deref(), tag.as_deref()).await {
Ok(resp) => {
quotes.set(resp.quotes);
total_pages.set(resp.total_pages.max(1));
loading.set(false);
}
Err(e) => {
error.set(Some(e.to_string()));
loading.set(false);
}
}
});
});
}
let on_page = {
let page = page.clone();
Callback::from(move |p: u32| page.set(p))
};
let on_author_input = {
let author_filter = author_filter.clone();
let page = page.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
author_filter.set(input.value());
page.set(1);
})
};
let on_tag_input = {
let tag_filter = tag_filter.clone();
let page = page.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
tag_filter.set(input.value());
page.set(1);
})
};
html! {
<div class="page-browse">
<h1 class="page-browse__title">{ "Browse Quotes" }</h1>
<div class="page-browse__filters">
<input
class="page-browse__filter-input"
type="text"
placeholder="Filter by author..."
value={(*author_filter).clone()}
oninput={on_author_input}
/>
<input
class="page-browse__filter-input"
type="text"
placeholder="Filter by tag..."
value={(*tag_filter).clone()}
oninput={on_tag_input}
/>
</div>
if *loading {
<p class="page-browse__loading">{ "Loading..." }</p>
} else if let Some(err) = (*error).clone() {
<ErrorDisplay message={err} />
} else if quotes.is_empty() {
<p class="page-browse__empty">{ "No quotes found." }</p>
} else {
<div class="page-browse__list">
{ for (*quotes).iter().map(|q| html! {
<QuoteCard quote={q.clone()} />
}) }
</div>
<Pagination
page={*page}
total_pages={*total_pages}
on_page={on_page}
/>
}
</div>
}
}

@ -1,8 +1,67 @@
//! Home page — displays a random quote.
//! Home page — displays a random quote on load.
use crate::api;
use crate::components::error::ErrorDisplay;
use crate::components::quote_card::QuoteCard;
use crate::Route;
use quotesdb::Quote;
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew_router::prelude::*;
/// Home page component. Fetches and displays a random quote on mount.
/// Home page component.
///
/// Fetches a random quote from the API on mount and displays it using
/// [`QuoteCard`]. Provides a "Browse all quotes" link to the browse page.
#[function_component(HomePage)]
pub fn home_page() -> Html {
html! { <p>{ "Loading..." }</p> }
let quote: UseStateHandle<Option<Quote>> = use_state(|| None);
let error: UseStateHandle<Option<String>> = use_state(|| None);
let loading = use_state(|| true);
{
let quote = quote.clone();
let error = error.clone();
let loading = loading.clone();
use_effect_with((), move |_| {
spawn_local(async move {
match api::get_random_quote().await {
Ok(q) => {
quote.set(Some(q));
loading.set(false);
}
Err(e) => {
error.set(Some(e.to_string()));
loading.set(false);
}
}
});
});
}
html! {
<div class="page-home">
<h1 class="page-home__title">{ "QuotesDB" }</h1>
<p class="page-home__subtitle">{ "A random quote for you:" }</p>
if *loading {
<p class="page-home__loading">{ "Loading..." }</p>
} else if let Some(err) = (*error).clone() {
<ErrorDisplay message={err} />
} else if let Some(q) = (*quote).clone() {
<QuoteCard quote={q} />
} else {
<p>{ "No quotes found." }</p>
}
<div class="page-home__actions">
<Link<Route> to={Route::Browse} classes="btn btn--primary">
{ "Browse all quotes" }
</Link<Route>>
<Link<Route> to={Route::Submit} classes="btn">
{ "Submit a quote" }
</Link<Route>>
</div>
</div>
}
}

@ -1,15 +1,397 @@
//! Quote detail page — view, edit, and delete a single quote.
use crate::api::{self, ApiError};
use crate::components::auth_modal::AuthModal;
use crate::components::error::ErrorDisplay;
use crate::components::quote_card::QuoteCard;
use crate::{storage, Route};
use quotesdb::{Quote, UpdateQuoteInput};
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yew_router::prelude::*;
/// Represents which modal/action is currently active.
#[derive(PartialEq, Clone)]
enum Action {
/// No active action.
None,
/// Edit auth modal shown.
EditAuth,
/// Edit form shown (auth has been entered).
EditForm,
/// Delete auth modal shown.
DeleteAuth,
}
/// Props for the [`QuotePage`] component.
#[derive(Properties, PartialEq)]
pub struct QuotePageProps {
/// The quote's unique ID from the route parameter.
/// The quote's unique ID extracted from the route parameter.
pub id: String,
}
/// Quote detail page. Fetches a single quote by ID and renders view/edit/delete actions.
/// Quote detail page.
///
/// Fetches a quote by ID, renders it with [`QuoteCard`], and provides
/// Edit and Delete actions each guarded by the [`AuthModal`] component.
///
/// - Edit: shows auth modal → edit form → `POST /api/quotes/:id` → re-fetch
/// - Delete: shows auth modal → `DELETE /api/quotes/:id` → navigate to `/browse`
/// - 403 errors clear the stored auth code and display an error message.
/// - 404 errors display a user-friendly "not found" message.
#[function_component(QuotePage)]
pub fn quote_page(props: &QuotePageProps) -> Html {
html! { <p>{ format!("Loading quote {}...", props.id) }</p> }
let id = props.id.clone();
let quote: UseStateHandle<Option<Quote>> = use_state(|| None);
let error: UseStateHandle<Option<String>> = use_state(|| None);
let loading = use_state(|| true);
let action = use_state(|| Action::None);
let action_error: UseStateHandle<Option<String>> = use_state(|| None);
// Edit form state — pre-filled from fetched quote
let edit_text = use_state(String::new);
let edit_author = use_state(String::new);
let edit_source = use_state(String::new);
let edit_date = use_state(String::new);
let edit_tags = use_state(String::new);
let navigator = use_navigator().unwrap();
// Fetch the quote on mount (and re-fetch when id changes)
{
let id = id.clone();
let quote = quote.clone();
let error = error.clone();
let loading = loading.clone();
use_effect_with(id.clone(), move |_| {
loading.set(true);
error.set(None);
spawn_local(async move {
match api::get_quote(&id).await {
Ok(q) => {
quote.set(Some(q));
loading.set(false);
}
Err(ApiError::Server { status: 404, .. }) => {
error.set(Some(
"Quote not found. It may have been deleted.".to_string(),
));
loading.set(false);
}
Err(e) => {
error.set(Some(e.to_string()));
loading.set(false);
}
}
});
});
}
// --- Edit auth modal submitted ---
let on_edit_auth = {
let id = id.clone();
let action = action.clone();
let action_error = action_error.clone();
let quote = quote.clone();
let edit_text = edit_text.clone();
let edit_author = edit_author.clone();
let edit_source = edit_source.clone();
let edit_date = edit_date.clone();
let edit_tags = edit_tags.clone();
Callback::from(move |code: String| {
// Pre-fill edit form with current values
if let Some(q) = (*quote).clone() {
edit_text.set(q.text.clone());
edit_author.set(q.author.clone());
edit_source.set(q.source.clone().unwrap_or_default());
edit_date.set(q.date.clone().unwrap_or_default());
edit_tags.set(q.tags.join(", "));
}
storage::set_auth_code(&id, &code);
action_error.set(None);
action.set(Action::EditForm);
})
};
// --- Edit form submitted ---
let on_edit_submit = {
let id = id.clone();
let quote = quote.clone();
let action = action.clone();
let action_error = action_error.clone();
let edit_text = edit_text.clone();
let edit_author = edit_author.clone();
let edit_source = edit_source.clone();
let edit_date = edit_date.clone();
let edit_tags = edit_tags.clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
let id = id.clone();
let quote = quote.clone();
let action = action.clone();
let action_error = action_error.clone();
let text = (*edit_text).clone();
let author = (*edit_author).clone();
let source = (*edit_source).clone();
let date = (*edit_date).clone();
let tags_str = (*edit_tags).clone();
spawn_local(async move {
let auth_code = match storage::get_auth_code(&id) {
Some(c) => c,
None => {
action_error.set(Some(
"No auth code found. Please re-enter your auth code.".to_string(),
));
action.set(Action::EditAuth);
return;
}
};
let tags: Vec<String> = tags_str
.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect();
let input = UpdateQuoteInput {
text: if text.is_empty() { None } else { Some(text) },
author: if author.is_empty() {
None
} else {
Some(author)
},
source: if source.is_empty() {
None
} else {
Some(source)
},
date: if date.is_empty() { None } else { Some(date) },
tags: Some(tags),
};
match api::update_quote(&id, &input, &auth_code).await {
Ok(updated) => {
storage::set_auth_code(&id, &auth_code);
quote.set(Some(updated));
action.set(Action::None);
action_error.set(None);
}
Err(ApiError::Server { status: 403, .. }) => {
storage::clear_auth_code(&id);
action_error.set(Some("Wrong auth code. Please try again.".to_string()));
action.set(Action::EditAuth);
}
Err(e) => {
action_error.set(Some(e.to_string()));
}
}
});
})
};
// --- Delete auth modal submitted ---
let on_delete_auth = {
let id = id.clone();
let action = action.clone();
let action_error = action_error.clone();
let navigator = navigator.clone();
Callback::from(move |code: String| {
let id = id.clone();
let action = action.clone();
let action_error = action_error.clone();
let navigator = navigator.clone();
storage::set_auth_code(&id, &code);
spawn_local(async move {
match api::delete_quote(&id, &code).await {
Ok(()) => {
storage::clear_auth_code(&id);
navigator.push(&Route::Browse);
}
Err(ApiError::Server { status: 403, .. }) => {
storage::clear_auth_code(&id);
action_error.set(Some("Wrong auth code. Please try again.".to_string()));
action.set(Action::DeleteAuth);
}
Err(e) => {
action_error.set(Some(e.to_string()));
action.set(Action::None);
}
}
});
})
};
let on_cancel = {
let action = action.clone();
let action_error = action_error.clone();
Callback::from(move |_| {
action.set(Action::None);
action_error.set(None);
})
};
html! {
<div class="page-quote">
if *loading {
<p class="page-quote__loading">{ "Loading..." }</p>
} else if let Some(err) = (*error).clone() {
<ErrorDisplay message={err} />
} else if let Some(q) = (*quote).clone() {
<QuoteCard quote={q.clone()} />
if let Some(action_err) = (*action_error).clone() {
<ErrorDisplay message={action_err} />
}
if *action == Action::None {
<div class="page-quote__actions">
<button
class="btn btn--primary"
onclick={{
let action = action.clone();
let id = id.clone();
let action_error = action_error.clone();
Callback::from(move |_| {
action_error.set(None);
action.set(Action::EditAuth);
// If we have a stored code, skip straight to form
if storage::get_auth_code(&id).is_some() {
action.set(Action::EditForm);
}
})
}}
>
{ "Edit" }
</button>
<button
class="btn btn--danger"
onclick={{
let action = action.clone();
let action_error = action_error.clone();
Callback::from(move |_| {
action_error.set(None);
action.set(Action::DeleteAuth);
})
}}
>
{ "Delete" }
</button>
</div>
}
if *action == Action::EditAuth {
<AuthModal
on_submit={on_edit_auth}
on_cancel={on_cancel.clone()}
prefill={storage::get_auth_code(&id)}
/>
}
if *action == Action::EditForm {
<form class="edit-form" onsubmit={on_edit_submit}>
<div class="edit-form__field">
<label class="edit-form__label">{ "Text" }</label>
<textarea
class="edit-form__textarea"
value={(*edit_text).clone()}
oninput={{
let edit_text = edit_text.clone();
Callback::from(move |e: InputEvent| {
let input: web_sys::HtmlTextAreaElement =
e.target_unchecked_into();
edit_text.set(input.value());
})
}}
/>
</div>
<div class="edit-form__field">
<label class="edit-form__label">{ "Author" }</label>
<input
class="edit-form__input"
type="text"
value={(*edit_author).clone()}
oninput={{
let edit_author = edit_author.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement =
e.target_unchecked_into();
edit_author.set(input.value());
})
}}
/>
</div>
<div class="edit-form__field">
<label class="edit-form__label">{ "Source (optional)" }</label>
<input
class="edit-form__input"
type="text"
value={(*edit_source).clone()}
oninput={{
let edit_source = edit_source.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement =
e.target_unchecked_into();
edit_source.set(input.value());
})
}}
/>
</div>
<div class="edit-form__field">
<label class="edit-form__label">{ "Date (optional, YYYY-MM-DD)" }</label>
<input
class="edit-form__input"
type="text"
value={(*edit_date).clone()}
oninput={{
let edit_date = edit_date.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement =
e.target_unchecked_into();
edit_date.set(input.value());
})
}}
/>
</div>
<div class="edit-form__field">
<label class="edit-form__label">{ "Tags (comma-separated)" }</label>
<input
class="edit-form__input"
type="text"
value={(*edit_tags).clone()}
oninput={{
let edit_tags = edit_tags.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement =
e.target_unchecked_into();
edit_tags.set(input.value());
})
}}
/>
</div>
<div class="edit-form__actions">
<button type="submit" class="btn btn--primary">{ "Save" }</button>
<button
type="button"
class="btn"
onclick={{
let on_cancel = on_cancel.clone();
Callback::from(move |_| on_cancel.emit(()))
}}
>
{ "Cancel" }
</button>
</div>
</form>
}
if *action == Action::DeleteAuth {
<AuthModal
on_submit={on_delete_auth}
on_cancel={on_cancel.clone()}
prefill={storage::get_auth_code(&id)}
/>
}
} else {
<p>{ "Quote not found." }</p>
}
</div>
}
}

@ -1,8 +1,271 @@
//! 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. Renders a form for creating a new quote.
/// 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 {
html! { <p>{ "Quote submission form" }</p> }
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() || author_val.is_empty() {
error.set(Some("Text and author are 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: 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::Submit} classes="btn">
{ "Submit another" }
</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 *" }</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());
})
}}
required=true
/>
</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, YYYY-MM-DD)" }</label>
<input
id="date"
class="submit-form__input"
type="text"
placeholder="e.g. 1884-01-01"
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">
{ "Custom auth code (optional)" }
</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>
}
}

Loading…
Cancel
Save