feat(quotesdb): implement UI app shell, API client, storage, and base components
- Add BrowserRouter with all 5 routes (Home, Browse, QuoteDetail, Author, Submit) - Implement typed API client (list, get, random, create, update, delete) - Implement sessionStorage auth code helpers (set/get/clear) - Add ErrorDisplay, QuoteCard, AuthModal, Pagination components - Add stub page components for initial compilation - Fix Cargo.toml: uuid js feature for wasm32, getrandom 0.3 wasm_js for rand dep, js-sys and Storage web-sys feature for API client and storage module Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
ea6fa981fc
commit
682d15b40d
@ -0,0 +1,178 @@
|
||||
//! Typed API client for the quotesdb backend.
|
||||
//!
|
||||
//! Provides async functions that wrap all quotesdb API endpoints using
|
||||
//! `gloo::net::http::Request`. All functions return `Result<T, ApiError>`.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! let quote = api::get_random_quote().await?;
|
||||
//! ```
|
||||
|
||||
use quotesdb::{CreateQuoteInput, Quote, UpdateQuoteInput};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Response type for `GET /api/quotes` (paginated list).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ListResponse {
|
||||
/// The quotes on the current page.
|
||||
pub quotes: Vec<Quote>,
|
||||
/// Current page number (1-indexed).
|
||||
pub page: u32,
|
||||
/// Total number of pages available.
|
||||
pub total_pages: u32,
|
||||
/// Total number of quotes matching the query.
|
||||
pub total_count: u32,
|
||||
}
|
||||
|
||||
/// Response type for `PUT /api/quotes` (create quote).
|
||||
///
|
||||
/// Includes the newly created quote and its auth code (only returned once).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CreateResponse {
|
||||
/// The created quote record.
|
||||
pub quote: Quote,
|
||||
/// The auth code required for future edits/deletes. Store this securely.
|
||||
pub auth_code: String,
|
||||
}
|
||||
|
||||
/// Errors that can occur during API calls.
|
||||
#[derive(Debug, thiserror::Error, Clone)]
|
||||
pub enum ApiError {
|
||||
/// A network-level error (e.g., connection refused, timeout).
|
||||
#[error("network error: {0}")]
|
||||
Network(String),
|
||||
/// The server responded with an error status code.
|
||||
#[error("server error {status}: {message}")]
|
||||
Server { status: u16, message: String },
|
||||
/// Failed to parse the response body as JSON.
|
||||
#[error("parse error: {0}")]
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
/// Fetch a paginated list of quotes.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `page` — 1-indexed page number.
|
||||
/// - `author` — Optional author name filter (URL-encoded automatically).
|
||||
/// - `tag` — Optional tag filter (URL-encoded automatically).
|
||||
pub async fn list_quotes(
|
||||
page: u32,
|
||||
author: Option<&str>,
|
||||
tag: Option<&str>,
|
||||
) -> Result<ListResponse, ApiError> {
|
||||
let mut url = format!("/api/quotes?page={page}");
|
||||
if let Some(a) = author {
|
||||
url.push_str(&format!("&author={}", js_sys::encode_uri_component(a)));
|
||||
}
|
||||
if let Some(t) = tag {
|
||||
url.push_str(&format!("&tag={}", js_sys::encode_uri_component(t)));
|
||||
}
|
||||
fetch_json(&url).await
|
||||
}
|
||||
|
||||
/// Fetch a single quote by its ID.
|
||||
///
|
||||
/// Returns `ApiError::Server { status: 404, .. }` if the quote does not exist.
|
||||
pub async fn get_quote(id: &str) -> Result<Quote, ApiError> {
|
||||
fetch_json(&format!("/api/quotes/{id}")).await
|
||||
}
|
||||
|
||||
/// Fetch a random quote from the database.
|
||||
pub async fn get_random_quote() -> Result<Quote, ApiError> {
|
||||
fetch_json("/api/quotes/random").await
|
||||
}
|
||||
|
||||
/// Create a new quote.
|
||||
///
|
||||
/// Returns the created quote and its auth code on success (HTTP 201).
|
||||
pub async fn create_quote(input: &CreateQuoteInput) -> Result<CreateResponse, ApiError> {
|
||||
let resp = gloo::net::http::Request::put("/api/quotes")
|
||||
.json(input)
|
||||
.map_err(|e| ApiError::Network(e.to_string()))?
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApiError::Network(e.to_string()))?;
|
||||
if resp.status() == 201 {
|
||||
resp.json()
|
||||
.await
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))
|
||||
} else {
|
||||
let msg = resp.text().await.unwrap_or_default();
|
||||
Err(ApiError::Server {
|
||||
status: resp.status(),
|
||||
message: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an existing quote.
|
||||
///
|
||||
/// Requires the correct `auth_code` in the `X-Auth-Code` header.
|
||||
/// Returns `ApiError::Server { status: 403, .. }` on wrong auth code.
|
||||
pub async fn update_quote(
|
||||
id: &str,
|
||||
input: &UpdateQuoteInput,
|
||||
auth_code: &str,
|
||||
) -> Result<Quote, ApiError> {
|
||||
let resp = gloo::net::http::Request::post(&format!("/api/quotes/{id}"))
|
||||
.header("X-Auth-Code", auth_code)
|
||||
.json(input)
|
||||
.map_err(|e| ApiError::Network(e.to_string()))?
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApiError::Network(e.to_string()))?;
|
||||
if resp.status() == 200 {
|
||||
resp.json()
|
||||
.await
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))
|
||||
} else {
|
||||
let msg = resp.text().await.unwrap_or_default();
|
||||
Err(ApiError::Server {
|
||||
status: resp.status(),
|
||||
message: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a quote by ID.
|
||||
///
|
||||
/// Requires the correct `auth_code` in the `X-Auth-Code` header.
|
||||
/// Returns `Ok(())` on HTTP 204. Returns `ApiError::Server { status: 403, .. }` on wrong auth code.
|
||||
pub async fn delete_quote(id: &str, auth_code: &str) -> Result<(), ApiError> {
|
||||
let resp = gloo::net::http::Request::delete(&format!("/api/quotes/{id}"))
|
||||
.header("X-Auth-Code", auth_code)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApiError::Network(e.to_string()))?;
|
||||
if resp.status() == 204 {
|
||||
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.
|
||||
async fn fetch_json<T: for<'de> serde::Deserialize<'de>>(url: &str) -> Result<T, ApiError> {
|
||||
let resp = gloo::net::http::Request::get(url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApiError::Network(e.to_string()))?;
|
||||
if resp.status() >= 200 && resp.status() < 300 {
|
||||
resp.json()
|
||||
.await
|
||||
.map_err(|e| ApiError::Parse(e.to_string()))
|
||||
} else {
|
||||
let msg = resp.text().await.unwrap_or_default();
|
||||
Err(ApiError::Server {
|
||||
status: resp.status(),
|
||||
message: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
//! AuthModal component — prompts the user to enter their auth code.
|
||||
//!
|
||||
//! Used on the Quote Detail page before edit or delete actions.
|
||||
|
||||
use yew::prelude::*;
|
||||
|
||||
/// Props for the [`AuthModal`] component.
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct AuthModalProps {
|
||||
/// Callback invoked with the entered auth code when the form is submitted.
|
||||
pub on_submit: Callback<String>,
|
||||
/// Callback invoked when the user cancels the modal.
|
||||
pub on_cancel: Callback<()>,
|
||||
/// Optional pre-filled value (e.g., from session storage).
|
||||
pub prefill: Option<String>,
|
||||
}
|
||||
|
||||
/// Modal dialog that prompts the user to enter an auth code.
|
||||
///
|
||||
/// Pre-fills the input with `prefill` if provided (e.g., from session storage).
|
||||
/// Calls `on_submit` with the entered value or `on_cancel` on dismissal.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// html! {
|
||||
/// <AuthModal
|
||||
/// on_submit={Callback::from(|code| log::info!("code: {code}"))}
|
||||
/// on_cancel={Callback::from(|_| {})}
|
||||
/// prefill={None}
|
||||
/// />
|
||||
/// }
|
||||
/// ```
|
||||
#[function_component(AuthModal)]
|
||||
pub fn auth_modal(props: &AuthModalProps) -> Html {
|
||||
let value = use_state(|| props.prefill.clone().unwrap_or_default());
|
||||
let oninput = {
|
||||
let value = value.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||
value.set(input.value());
|
||||
})
|
||||
};
|
||||
let onsubmit = {
|
||||
let value = value.clone();
|
||||
let on_submit = props.on_submit.clone();
|
||||
Callback::from(move |e: SubmitEvent| {
|
||||
e.prevent_default();
|
||||
on_submit.emit((*value).clone());
|
||||
})
|
||||
};
|
||||
let on_cancel = props.on_cancel.clone();
|
||||
html! {
|
||||
<div class="auth-modal__overlay">
|
||||
<div class="auth-modal">
|
||||
<h2 class="auth-modal__title">{ "Enter Auth Code" }</h2>
|
||||
<form onsubmit={onsubmit}>
|
||||
<input
|
||||
class="auth-modal__input"
|
||||
type="text"
|
||||
placeholder="word-word-word-word"
|
||||
value={(*value).clone()}
|
||||
oninput={oninput}
|
||||
/>
|
||||
<div class="auth-modal__actions">
|
||||
<button type="submit" class="btn btn--primary">{ "Confirm" }</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
onclick={Callback::from(move |_| on_cancel.emit(()))}
|
||||
>
|
||||
{ "Cancel" }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
//! Error display component for surfacing API and application errors to users.
|
||||
|
||||
use yew::prelude::*;
|
||||
|
||||
/// Props for the [`ErrorDisplay`] component.
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct ErrorProps {
|
||||
/// The error message to display.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Displays a user-visible error message in a styled container.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// html! { <ErrorDisplay message="Something went wrong." /> }
|
||||
/// ```
|
||||
#[function_component(ErrorDisplay)]
|
||||
pub fn error_display(props: &ErrorProps) -> Html {
|
||||
html! {
|
||||
<div class="error-display">
|
||||
<p class="error-display__message">{ &props.message }</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
//! Reusable UI components for the quotesdb frontend.
|
||||
//!
|
||||
//! Each component lives in its own submodule and is a Yew `#[function_component]`.
|
||||
|
||||
pub mod auth_modal;
|
||||
pub mod error;
|
||||
pub mod pagination;
|
||||
pub mod quote_card;
|
||||
@ -0,0 +1,54 @@
|
||||
//! Pagination component — previous/next page controls.
|
||||
|
||||
use yew::prelude::*;
|
||||
|
||||
/// Props for the [`Pagination`] component.
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct PaginationProps {
|
||||
/// The current page number (1-indexed).
|
||||
pub page: u32,
|
||||
/// The total number of pages.
|
||||
pub total_pages: u32,
|
||||
/// Callback invoked with the new page number when the user navigates.
|
||||
pub on_page: Callback<u32>,
|
||||
}
|
||||
|
||||
/// Renders previous/next page controls with the current page indicator.
|
||||
///
|
||||
/// Disables the "Previous" button on page 1 and "Next" on the last page.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// html! {
|
||||
/// <Pagination page={1} total_pages={5} on_page={Callback::from(|p| log::info!("{p}"))} />
|
||||
/// }
|
||||
/// ```
|
||||
#[function_component(Pagination)]
|
||||
pub fn pagination(props: &PaginationProps) -> Html {
|
||||
let prev = props.page > 1;
|
||||
let next = props.page < props.total_pages;
|
||||
let on_prev = {
|
||||
let cb = props.on_page.clone();
|
||||
let p = props.page;
|
||||
Callback::from(move |_| cb.emit(p - 1))
|
||||
};
|
||||
let on_next = {
|
||||
let cb = props.on_page.clone();
|
||||
let p = props.page;
|
||||
Callback::from(move |_| cb.emit(p + 1))
|
||||
};
|
||||
html! {
|
||||
<div class="pagination">
|
||||
<button class="pagination__btn" disabled={!prev} onclick={on_prev}>
|
||||
{ "← Prev" }
|
||||
</button>
|
||||
<span class="pagination__info">
|
||||
{ format!("Page {} of {}", props.page, props.total_pages) }
|
||||
</span>
|
||||
<button class="pagination__btn" disabled={!next} onclick={on_next}>
|
||||
{ "Next →" }
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
//! QuoteCard component — renders a single quote in a card layout.
|
||||
use crate::Route;
|
||||
use quotesdb::Quote;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
/// Props for the [`QuoteCard`] component.
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct QuoteCardProps {
|
||||
/// The quote to display.
|
||||
pub quote: Quote,
|
||||
}
|
||||
|
||||
/// Renders a quote in a styled card with author, source, tags, and a "View" link.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// html! { <QuoteCard quote={my_quote} /> }
|
||||
/// ```
|
||||
#[function_component(QuoteCard)]
|
||||
pub fn quote_card(props: &QuoteCardProps) -> Html {
|
||||
let q = &props.quote;
|
||||
html! {
|
||||
<div class="quote-card">
|
||||
<blockquote class="quote-card__text">{ &q.text }</blockquote>
|
||||
<footer class="quote-card__footer">
|
||||
<Link<Route> to={Route::Author { name: q.author.clone() }} classes="quote-card__author">
|
||||
{ format!("— {}", q.author) }
|
||||
</Link<Route>>
|
||||
if let Some(src) = &q.source {
|
||||
<span class="quote-card__source">{ format!(", {src}") }</span>
|
||||
}
|
||||
</footer>
|
||||
if !q.tags.is_empty() {
|
||||
<div class="quote-card__tags">
|
||||
{ for q.tags.iter().map(|t| html! {
|
||||
<span class="quote-card__tag">{ t }</span>
|
||||
}) }
|
||||
</div>
|
||||
}
|
||||
<Link<Route> to={Route::QuoteDetail { id: q.id.clone() }} classes="quote-card__link">
|
||||
{ "View" }
|
||||
</Link<Route>>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,82 @@
|
||||
//! UI binary entrypoint.
|
||||
//! UI binary entrypoint for quotesdb.
|
||||
//!
|
||||
//! Compiled to WebAssembly via Trunk targeting `wasm32-unknown-unknown`.
|
||||
//! Runs the Yew frontend application.
|
||||
//! Runs the Yew single-page application with client-side routing.
|
||||
|
||||
fn main() {}
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
||||
/// Application routes matching the frontend route definitions in the design spec.
|
||||
///
|
||||
/// Routes map to page components. The `NotFound` variant catches all unmatched paths.
|
||||
#[derive(Clone, Routable, PartialEq)]
|
||||
pub enum Route {
|
||||
/// Home page — displays a random quote.
|
||||
#[at("/")]
|
||||
Home,
|
||||
/// Browse page — paginated list of quotes with filters.
|
||||
#[at("/browse")]
|
||||
Browse,
|
||||
/// Single quote view/edit/delete page.
|
||||
#[at("/quotes/:id")]
|
||||
QuoteDetail { id: String },
|
||||
/// All quotes by a specific author.
|
||||
#[at("/author/:name")]
|
||||
Author { name: String },
|
||||
/// New quote submission form.
|
||||
#[at("/submit")]
|
||||
Submit,
|
||||
/// Catch-all 404 page.
|
||||
#[not_found]
|
||||
#[at("/404")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
/// Route switch function — maps each `Route` variant to its page component.
|
||||
fn switch(routes: Route) -> Html {
|
||||
match routes {
|
||||
Route::Home => html! { <pages::home::HomePage /> },
|
||||
Route::Browse => html! { <pages::browse::BrowsePage /> },
|
||||
Route::QuoteDetail { id } => html! { <pages::quote::QuotePage id={id} /> },
|
||||
Route::Author { name } => html! { <pages::author::AuthorPage name={name} /> },
|
||||
Route::Submit => html! { <pages::submit::SubmitPage /> },
|
||||
Route::NotFound => html! {
|
||||
<div class="page-not-found">
|
||||
<h1>{ "404 — Page Not Found" }</h1>
|
||||
<p>{ "The page you are looking for does not exist." }</p>
|
||||
<a href="/">{ "Go home" }</a>
|
||||
</div>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Root application component.
|
||||
///
|
||||
/// Wraps the entire application in a `BrowserRouter` and renders the
|
||||
/// route-switched page components via `Switch`.
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
html! {
|
||||
<BrowserRouter>
|
||||
<nav class="nav">
|
||||
<Link<Route> to={Route::Home} classes="nav__brand">{ "QuotesDB" }</Link<Route>>
|
||||
<div class="nav__links">
|
||||
<Link<Route> to={Route::Browse} classes="nav__link">{ "Browse" }</Link<Route>>
|
||||
<Link<Route> to={Route::Submit} classes="nav__link">{ "Submit" }</Link<Route>>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="main-content">
|
||||
<Switch<Route> render={switch} />
|
||||
</main>
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
||||
mod api;
|
||||
mod components;
|
||||
mod pages;
|
||||
mod storage;
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
//! Author page — all quotes by a specific author.
|
||||
use yew::prelude::*;
|
||||
|
||||
/// Props for the [`AuthorPage`] component.
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct AuthorPageProps {
|
||||
/// The author name from the route parameter.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Author page component. Lists all quotes attributed to the given author.
|
||||
#[function_component(AuthorPage)]
|
||||
pub fn author_page(props: &AuthorPageProps) -> Html {
|
||||
html! { <p>{ format!("Loading quotes by {}...", props.name) }</p> }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
//! Browse page — paginated quote list with author and tag filters.
|
||||
use yew::prelude::*;
|
||||
|
||||
/// Browse page component. Displays paginated quotes with filter controls.
|
||||
#[function_component(BrowsePage)]
|
||||
pub fn browse_page() -> Html {
|
||||
html! { <p>{ "Loading..." }</p> }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
//! Home page — displays a random quote.
|
||||
use yew::prelude::*;
|
||||
|
||||
/// Home page component. Fetches and displays a random quote on mount.
|
||||
#[function_component(HomePage)]
|
||||
pub fn home_page() -> Html {
|
||||
html! { <p>{ "Loading..." }</p> }
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
//! Page components for the quotesdb frontend.
|
||||
//!
|
||||
//! Each page corresponds to a route in the [`crate::Route`] enum.
|
||||
|
||||
pub mod author;
|
||||
pub mod browse;
|
||||
pub mod home;
|
||||
pub mod quote;
|
||||
pub mod submit;
|
||||
@ -0,0 +1,15 @@
|
||||
//! Quote detail page — view, edit, and delete a single quote.
|
||||
use yew::prelude::*;
|
||||
|
||||
/// Props for the [`QuotePage`] component.
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct QuotePageProps {
|
||||
/// The quote's unique ID from the route parameter.
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
/// Quote detail page. Fetches a single quote by ID and renders view/edit/delete actions.
|
||||
#[function_component(QuotePage)]
|
||||
pub fn quote_page(props: &QuotePageProps) -> Html {
|
||||
html! { <p>{ format!("Loading quote {}...", props.id) }</p> }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
//! Submit page — new quote submission form.
|
||||
use yew::prelude::*;
|
||||
|
||||
/// Submit page component. Renders a form for creating a new quote.
|
||||
#[function_component(SubmitPage)]
|
||||
pub fn submit_page() -> Html {
|
||||
html! { <p>{ "Quote submission form" }</p> }
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
//! Session storage helpers for persisting quote auth codes within a browser tab.
|
||||
//!
|
||||
//! Auth codes are stored in `sessionStorage` under the key `auth_code_{quote_id}`.
|
||||
//! Session storage is tab-scoped and cleared when the tab closes, making it
|
||||
//! appropriate for short-lived auth code persistence.
|
||||
//!
|
||||
//! This module must only be used from `wasm32-unknown-unknown` targets (the UI binary).
|
||||
|
||||
/// Persist an auth code for a quote in `sessionStorage`.
|
||||
///
|
||||
/// The key format is `auth_code_{quote_id}`. Silently no-ops if the browser
|
||||
/// `sessionStorage` API is unavailable.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `quote_id` — The quote's unique ID.
|
||||
/// - `auth_code` — The auth code to store.
|
||||
pub fn set_auth_code(quote_id: &str, auth_code: &str) {
|
||||
if let Some(storage) = web_sys::window()
|
||||
.and_then(|w| w.session_storage().ok())
|
||||
.flatten()
|
||||
{
|
||||
let _ = storage.set_item(&format!("auth_code_{quote_id}"), auth_code);
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the stored auth code for a quote from `sessionStorage`.
|
||||
///
|
||||
/// Returns `None` if no code is stored for this quote ID or if `sessionStorage`
|
||||
/// is unavailable.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `quote_id` — The quote's unique ID.
|
||||
pub fn get_auth_code(quote_id: &str) -> Option<String> {
|
||||
web_sys::window()
|
||||
.and_then(|w| w.session_storage().ok())
|
||||
.flatten()
|
||||
.and_then(|s| s.get_item(&format!("auth_code_{quote_id}")).ok())
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Remove the stored auth code for a quote from `sessionStorage`.
|
||||
///
|
||||
/// Silently no-ops if no code is stored or if `sessionStorage` is unavailable.
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `quote_id` — The quote's unique ID.
|
||||
pub fn clear_auth_code(quote_id: &str) {
|
||||
if let Some(storage) = web_sys::window()
|
||||
.and_then(|w| w.session_storage().ok())
|
||||
.flatten()
|
||||
{
|
||||
let _ = storage.remove_item(&format!("auth_code_{quote_id}"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue