diff --git a/quotesdb/src/bin/ui/api.rs b/quotesdb/src/bin/ui/api.rs index 30bd49f..01fb23b 100644 --- a/quotesdb/src/bin/ui/api.rs +++ b/quotesdb/src/bin/ui/api.rs @@ -343,6 +343,51 @@ pub async fn admin_unlock(admin_code: &str) -> Result { } } +/// Request body for `POST /api/quotes/:id/report`. +#[derive(Serialize)] +struct ReportBody { + /// Optional human-readable reason for the report (max 256 characters). + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, + /// Cloudflare Turnstile CAPTCHA token obtained from the widget. + captcha_token: String, +} + +/// Submit a report for a quote. +/// +/// Posts `{ reason?, captcha_token }` to `POST /api/quotes/:id/report`. +/// Returns `Ok(())` on HTTP 201. Returns [`ApiError::Server`] for other errors. +/// +/// # Arguments +/// - `id` — the quote's NanoID. +/// - `reason` — optional user-provided reason text (max 256 characters). +/// - `captcha_token` — Cloudflare Turnstile token from the solved CAPTCHA widget. +pub async fn report_quote( + id: &str, + reason: Option, + captcha_token: String, +) -> Result<(), ApiError> { + let body = ReportBody { + reason, + captcha_token, + }; + let resp = gloo::net::http::Request::post(&format!("/api/quotes/{id}/report")) + .json(&body) + .map_err(|e| ApiError::Network(e.to_string()))? + .send() + .await + .map_err(|e| ApiError::Network(e.to_string()))?; + if resp.status() == 201 { + Ok(()) + } else { + let msg = resp.text().await.unwrap_or_default(); + Err(ApiError::Server { + status: resp.status(), + message: msg, + }) + } +} + /// Internal helper: GET a URL and deserialise the response body as JSON. /// /// Returns `ApiError` on non-2xx status or deserialisation failure. diff --git a/quotesdb/src/bin/ui/components/mod.rs b/quotesdb/src/bin/ui/components/mod.rs index 05f88eb..412ff88 100644 --- a/quotesdb/src/bin/ui/components/mod.rs +++ b/quotesdb/src/bin/ui/components/mod.rs @@ -6,3 +6,4 @@ pub mod auth_modal; pub mod error; pub mod pagination; pub mod quote_card; +pub mod report_modal; diff --git a/quotesdb/src/bin/ui/components/report_modal.rs b/quotesdb/src/bin/ui/components/report_modal.rs new file mode 100644 index 0000000..de24e87 --- /dev/null +++ b/quotesdb/src/bin/ui/components/report_modal.rs @@ -0,0 +1,212 @@ +//! ReportModal component — lets users report a quote with an optional reason +//! and a Cloudflare Turnstile CAPTCHA. +//! +//! The modal renders: +//! - A textarea for an optional reason (max 256 characters, with a live counter). +//! - A Cloudflare Turnstile widget that sets a hidden input once solved. +//! - Submit (disabled until the CAPTCHA is solved) and Cancel buttons. +//! +//! Turnstile is loaded globally via the `