From 08eb9d398b47bbbe116d93e0cb5b6f8e061ac360 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Sun, 8 Mar 2026 20:32:13 -0700 Subject: [PATCH] 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 --- quotesdb/src/bin/ui/pages/quote.rs | 78 +++++++++++++++++++++++++++++- quotesdb/src/bin/ui/style.css | 12 +++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/quotesdb/src/bin/ui/pages/quote.rs b/quotesdb/src/bin/ui/pages/quote.rs index 6932388..7a3f4c0 100644 --- a/quotesdb/src/bin/ui/pages/quote.rs +++ b/quotesdb/src/bin/ui/pages/quote.rs @@ -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::components::auth_modal::AuthModal; @@ -23,6 +23,8 @@ enum Action { EditForm, /// Delete auth modal shown. DeleteAuth, + /// Hide/unhide auth modal shown. + HideAuth, /// Report modal shown. Report, } @@ -37,10 +39,11 @@ pub struct QuotePageProps { /// Quote detail page. /// /// 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 /// - 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 /// - 403 errors clear the stored auth code and display an error 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 action = action.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(q) = (*quote).clone() { + if q.hidden { +
+ { "Hidden — this quote is not shown in listings" } +
+ } + if let Some(auth) = (*new_auth).clone() { @@ -339,6 +392,19 @@ pub fn quote_page(props: &QuotePageProps) -> Html { > { "Delete" } +