feat(quotesdb): add hidden toggle on quote detail page

- Add HideAuth action variant to the Action enum
- Show yellow badge when quote.hidden is true
- Add Hide/Make Public toggle button in the actions bar
- Reuse AuthModal for the hide/unhide auth prompt
- Call POST /api/quotes/:id with { hidden: !current } and X-Auth-Code
- Update quote state on success; re-prompt on 403
- Add .page-quote__hidden-badge CSS styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
Elijah Voigt 3 months ago
parent 409afffa63
commit 08eb9d398b

@ -1,4 +1,4 @@
//! Quote detail page — view, edit, delete, and report a single quote. //! Quote detail page — view, edit, delete, hide, and report a single quote.
use crate::api::{self, ApiError}; use crate::api::{self, ApiError};
use crate::components::auth_modal::AuthModal; use crate::components::auth_modal::AuthModal;
@ -23,6 +23,8 @@ enum Action {
EditForm, EditForm,
/// Delete auth modal shown. /// Delete auth modal shown.
DeleteAuth, DeleteAuth,
/// Hide/unhide auth modal shown.
HideAuth,
/// Report modal shown. /// Report modal shown.
Report, Report,
} }
@ -37,10 +39,11 @@ pub struct QuotePageProps {
/// Quote detail page. /// Quote detail page.
/// ///
/// Fetches a quote by ID, renders it with [`QuoteCard`], and provides /// Fetches a quote by ID, renders it with [`QuoteCard`], and provides
/// Edit, Delete, and Report actions. /// Edit, Delete, Hide/Unhide, and Report actions.
/// ///
/// - Edit: shows auth modal → edit form → `POST /api/quotes/:id` → re-fetch /// - Edit: shows auth modal → edit form → `POST /api/quotes/:id` → re-fetch
/// - Delete: shows auth modal → `DELETE /api/quotes/:id` → navigate to `/browse` /// - Delete: shows auth modal → `DELETE /api/quotes/:id` → navigate to `/browse`
/// - Hide/Unhide: shows auth modal → `POST /api/quotes/:id` `{ hidden: bool }` → update state
/// - Report: shows [`ReportModal`] → `POST /api/quotes/:id/report` → success message /// - Report: shows [`ReportModal`] → `POST /api/quotes/:id/report` → success message
/// - 403 errors clear the stored auth code and display an error message. /// - 403 errors clear the stored auth code and display an error message.
/// - 404 errors display a user-friendly "not found" message. /// - 404 errors display a user-friendly "not found" message.
@ -239,6 +242,50 @@ pub fn quote_page(props: &QuotePageProps) -> Html {
}) })
}; };
// --- Hide/unhide auth modal submitted ---
let on_hide_auth = {
let id = id.clone();
let action = action.clone();
let action_error = action_error.clone();
let quote = quote.clone();
Callback::from(move |code: String| {
let id = id.clone();
let action = action.clone();
let action_error = action_error.clone();
let quote = quote.clone();
// Determine the desired hidden state from current quote value.
let new_hidden = (*quote).as_ref().map(|q| !q.hidden).unwrap_or(true);
storage::set_auth_code(&id, &code);
spawn_local(async move {
let input = UpdateQuoteInput {
text: None,
author: None,
source: None,
date: None,
tags: None,
hidden: Some(new_hidden),
};
match api::update_quote(&id, &input, &code).await {
Ok(updated) => {
storage::set_auth_code(&id, &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::HideAuth);
}
Err(e) => {
action_error.set(Some(e.to_string()));
action.set(Action::None);
}
}
});
})
};
let on_cancel = { let on_cancel = {
let action = action.clone(); let action = action.clone();
let action_error = action_error.clone(); let action_error = action_error.clone();
@ -284,6 +331,12 @@ pub fn quote_page(props: &QuotePageProps) -> Html {
} else if let Some(err) = (*error).clone() { } else if let Some(err) = (*error).clone() {
<ErrorDisplay message={err} /> <ErrorDisplay message={err} />
} else if let Some(q) = (*quote).clone() { } else if let Some(q) = (*quote).clone() {
if q.hidden {
<div class="page-quote__hidden-badge">
{ "Hidden — this quote is not shown in listings" }
</div>
}
<QuoteCard quote={q.clone()} /> <QuoteCard quote={q.clone()} />
if let Some(auth) = (*new_auth).clone() { if let Some(auth) = (*new_auth).clone() {
@ -339,6 +392,19 @@ pub fn quote_page(props: &QuotePageProps) -> Html {
> >
{ "Delete" } { "Delete" }
</button> </button>
<button
class="btn btn--muted"
onclick={{
let action = action.clone();
let action_error = action_error.clone();
Callback::from(move |_| {
action_error.set(None);
action.set(Action::HideAuth);
})
}}
>
{ if q.hidden { "Make Public" } else { "Hide" } }
</button>
<button <button
class="btn btn--muted" class="btn btn--muted"
onclick={{ onclick={{
@ -470,6 +536,14 @@ pub fn quote_page(props: &QuotePageProps) -> Html {
/> />
} }
if *action == Action::HideAuth {
<AuthModal
on_submit={on_hide_auth}
on_cancel={on_cancel.clone()}
prefill={storage::get_auth_code(&id)}
/>
}
if *action == Action::Report { if *action == Action::Report {
<ReportModal <ReportModal
on_submit={on_report_submit} on_submit={on_report_submit}

@ -430,6 +430,18 @@ code {
justify-content: flex-end; justify-content: flex-end;
} }
/* ── Quote Detail — hidden badge ───────────────────────────── */
.page-quote__hidden-badge {
background: #fefce8;
border: 1px solid #fde047;
border-radius: var(--radius);
padding: 0.75rem 1rem;
margin-bottom: 1rem;
color: #854d0e;
font-size: 0.9rem;
font-weight: 500;
}
/* ── Quote Detail — report success notice ──────────────────── */ /* ── Quote Detail — report success notice ──────────────────── */
.page-quote__report-success { .page-quote__report-success {
background: #f0fdf4; background: #f0fdf4;

Loading…
Cancel
Save