Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385use dioxus::prelude::ServerFnError;use dioxus::prelude::*;use polymodel_api::space_polymodel::library;
use crate::Route;use crate::examples::{HERO_SAMPLE_HANDLES, first_hero_sample_handle};use crate::session::SessionIdentity;use crate::thing_card::ThingCard;
/// Cookie set client-side when the logged-out hero band is dismissed; read/// server-side so SSR omits the band on return without a hydration flash.const HERO_DISMISS_COOKIE: &str = "pm_hero_dismissed";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]enum FeedAlgorithm { Recent, Hot, Following,}
impl FeedAlgorithm { fn as_str(self) -> &'static str { match self { Self::Recent => "recent", Self::Hot => "hot", Self::Following => "following", } }
fn label(self) -> &'static str { match self { Self::Recent => "Recent", Self::Hot => "Hot", Self::Following => "Following", } }}
#[derive(Clone, Debug, PartialEq, Eq)]enum BrowseFeedState { Loading, Error(String), Empty, Populated(Vec<library::ThingViewBasic>),}
fn browse_feed_state( pending: bool, result: Option<&Result<Vec<library::ThingViewBasic>, String>>,) -> BrowseFeedState { if pending && result.is_none() { return BrowseFeedState::Loading; }
match result { Some(Ok(things)) if things.is_empty() => BrowseFeedState::Empty, Some(Ok(things)) => BrowseFeedState::Populated(things.clone()), Some(Err(error)) => BrowseFeedState::Error(error.clone()), None => BrowseFeedState::Loading, }}
/// Whether the logged-out hero band should render. It shows only to a/// definitively anonymous visitor who has not dismissed it; an unresolved/// session (`Unknown`), a server-evaluated dismissal seed of `true`, or a/// live client-read dismissal cookie each suppress it.////// `hero_dismissed_seed` is the SSR/first-paint decision (server cookie read,/// cached through hydration), so the band is correct in the initial markup/// with no flash. `client_dismissed` is the post-hydration backstop: a fresh/// read of the live cookie that keeps the band suppressed across client-side/// navigation, where the cached seed would otherwise still read `false`.fn should_show_hero( session: &SessionIdentity, hero_dismissed_seed: Option<&Result<bool, ServerFnError>>, client_dismissed: bool,) -> bool { !client_dismissed && matches!(session, SessionIdentity::Anonymous) && !matches!(hero_dismissed_seed, Some(Ok(true)))}
/// True when the cookie header carries the named flag (presence only; the/// dismiss control always sets it to `1`). Pure so it stays unit-testable.// Only reached from the wasm client read and the unit tests; on native// non-test builds it is legitimately unreferenced.#[cfg_attr(not(any(target_arch = "wasm32", test)), allow(dead_code))]fn cookie_has_flag(cookie_header: &str, name: &str) -> bool { cookie_header .split(';') .filter_map(|pair| pair.split_once('=')) .any(|(key, _)| key.trim() == name)}
/// Read the dismissal cookie from the browser on the client. Returns `false`/// on the server (where SSR's `hero_dismissed_seed` owns the decision) and on/// non-wasm targets.#[cfg(target_arch = "wasm32")]fn client_cookie_dismissed() -> bool { use wasm_bindgen::JsValue;
let Some(document) = web_sys::window().and_then(|window| window.document()) else { return false; }; // `Document::cookie` lives behind the `HtmlDocument` web-sys feature, which // isn't enabled; read the property reflectively to avoid widening features. let cookie = js_sys::Reflect::get(document.as_ref(), &JsValue::from_str("cookie")) .ok() .and_then(|value| value.as_string()) .unwrap_or_default(); cookie_has_flag(&cookie, HERO_DISMISS_COOKIE)}
#[cfg(not(target_arch = "wasm32"))]fn client_cookie_dismissed() -> bool { false}
#[allow(clippy::useless_format)]#[component]pub(crate) fn Browse() -> Element { let selected_algorithm = use_signal(|| FeedAlgorithm::Hot);
let mut feed = use_server_future(move || { let algorithm = selected_algorithm.read().as_str().to_owned(); feed_seed(algorithm, 24) })?;
let feed_result = feed.read(); let mapped = feed_result.as_ref().map(|r| match r { Ok(things) => Ok(things.clone()), Err(ServerFnError::ServerError { message, .. }) => Err(message.clone()), Err(other) => Err(other.to_string()), }); let feed_state = browse_feed_state(feed.pending(), mapped.as_ref());
// Show the logged-out hero band only to definitively anonymous visitors who // haven't dismissed it. Both inputs resolve server-side during SSR (session // via the App seed, dismissal via the cookie server fn), so the band is // present or absent in the initial markup with no post-hydration flash. let session = use_context::<Signal<SessionIdentity>>(); let hero_dismissed = use_server_future(hero_dismissed_seed)?; let client_dismissed = use_hook(client_cookie_dismissed); let show_hero = should_show_hero(&session.read(), hero_dismissed().as_ref(), client_dismissed);
rsx! { main { class: "browse-page",
if show_hero { HeroBand {} }
section { class: "browse-feed", aria_label: "Catalog feed", div { class: "tab-row", role: "tablist", aria_label: "Feeds", FeedTab { algorithm: FeedAlgorithm::Hot, selected_algorithm } FeedTab { algorithm: FeedAlgorithm::Recent, selected_algorithm } FeedTab { algorithm: FeedAlgorithm::Following, selected_algorithm } }
section { class: "browse-results-grid", aria_label: "Browse results", match feed_state { BrowseFeedState::Loading => rsx! { for index in 0..6 { div { key: "skeleton-{index}", class: "state-card loading-state", span { class: "status-pill", "Loading" } div { class: "skeleton-line skeleton-wide" } div { class: "skeleton-line" } div { class: "skeleton-box" } } } }, BrowseFeedState::Error(error) => rsx! { div { class: "state-card error-state browse-state-card", p { "{error}" } button { class: "button button-secondary", onclick: move |_| feed.restart(), "Retry" } } }, BrowseFeedState::Empty => rsx! { div { class: "state-card empty-state browse-state-card", span { class: "status-pill status-muted", "Empty" } if *selected_algorithm.read() == FeedAlgorithm::Following && matches!(&*session.read(), SessionIdentity::Anonymous) { h2 { "Sign in to build a following feed" } p { "Follow makers to collect their latest Polymodel projects here." } form { class: "hero-band-signin", action: "/oauth/start", method: "get", input { r#type: "hidden", name: "return_to", value: "/" } button { class: "button button-primary", r#type: "submit", "Sign in" } } } else if *selected_algorithm.read() == FeedAlgorithm::Following { h2 { "No followed projects yet" } p { "Follow makers from their profiles and their projects will appear here." } } else { h2 { "No projects yet" } p { "Polymodel reached the appview feed, but this local index has no projects for the selected feed yet." } } } }, BrowseFeedState::Populated(things) => rsx! { for thing in things { BrowseThingCard { key: "{thing.uri}", thing } } }, } } } } }}
#[component]fn BrowseThingCard(thing: library::ThingViewBasic) -> Element { rsx! { ThingCard { thing } }}
#[component]fn FeedTab(algorithm: FeedAlgorithm, selected_algorithm: Signal<FeedAlgorithm>) -> Element { let is_active = *selected_algorithm.read() == algorithm; let class = if is_active { "tab is-active" } else { "tab" };
rsx! { button { class, role: "tab", aria_selected: "{is_active}", onclick: move |_| selected_algorithm.set(algorithm), "{algorithm.label()}" } }}
#[component]fn HeroBand() -> Element { // Rendered as raw HTML so the dismiss control carries a literal `onclick` // that fires before (or without) wasm hydration. A Dioxus event handler // would require the wasm bundle to be live, defeating the no-flash goal. // // Hiding via `style.display` rather than removing the node keeps Dioxus // hydration reconciliation intact: `style` is an attribute Dioxus never // rendered here, so re-renders of this same node (feed loads, tab // switches) won't clobber the hidden state. A *remount* (client-side // navigation back to `/` builds a fresh node) does drop the inline style, // so the cookie this sets is also read client-side in `should_show_hero` // to keep a dismissed band suppressed across navigation. let dismiss_html = format!( "<button type=\"button\" class=\"hero-dismiss\" aria-label=\"Dismiss introduction\" \ onclick=\"document.cookie='{cookie}=1; path=/; max-age=31536000; samesite=lax'; \ var b=this.closest('.hero-band'); if(b){{b.style.display='none';}}\">×</button>", cookie = HERO_DISMISS_COOKIE ); let sample_handles = serde_json::to_string(HERO_SAMPLE_HANDLES) .expect("hero sample handles should serialize as JSON"); let rotation_js = format!( r#" (() => {{ const samples = {sample_handles}; const input = document.currentScript.closest('.hero-band')?.querySelector('.hero-band-input'); if (!input || samples.length < 2) return; let index = Math.floor(Math.random() * samples.length); input.placeholder = samples[index]; window.setInterval(() => {{ index = (index + 1) % samples.length; input.placeholder = samples[index]; }}, 30000); }})(); "#, );
rsx! { section { class: "hero-band blueprint-panel", aria_label: "About Polymodel", div { class: "hero-band-body", p { class: "hero-band-pitch", "Share your creations. Make things, remix them. Without getting locked in to a closed platform." } div { class: "hero-band-actions", form { class: "hero-band-signin", action: "/oauth/start", method: "get", input { r#type: "hidden", name: "return_to", value: "/" } input { class: "hero-band-input", r#type: "text", name: "identifier", placeholder: "{first_hero_sample_handle()}", aria_label: "Bluesky handle", } button { class: "button button-secondary", r#type: "submit", "Sign in" } } Link { class: "hero-band-about", to: Route::About {}, "what's this?" } } } div { class: "hero-band-dismiss", dangerous_inner_html: dismiss_html } script { dangerous_inner_html: rotation_js } } }}
#[server]async fn hero_dismissed_seed() -> Result<bool, ServerFnError> { use axum_extra::extract::cookie::CookieJar; use dioxus::prelude::dioxus_fullstack::FullstackContext;
let context = FullstackContext::current() .ok_or_else(|| ServerFnError::new("missing Dioxus fullstack request context"))?; let parts = context.parts_mut().clone(); let jar = CookieJar::from_headers(&parts.headers); Ok(jar.get(HERO_DISMISS_COOKIE).is_some())}
#[server]async fn feed_seed( algorithm: String, limit: i64,) -> Result<Vec<library::ThingViewBasic>, ServerFnError> { let feed = crate::appview::ssr::get_feed_from_fullstack_context(&algorithm, limit).await?; Ok(feed.items.into_iter().map(|item| item.thing).collect())}
#[cfg(test)]mod tests { use super::*; use crate::thing_card::thing_card_fixtures;
#[test] fn browse_state_distinguishes_loading_empty_error_and_populated() { assert_eq!(browse_feed_state(true, None), BrowseFeedState::Loading); assert_eq!( browse_feed_state(false, Some(&Ok(vec![]))), BrowseFeedState::Empty ); assert_eq!( browse_feed_state(false, Some(&Err("boom".to_string()))), BrowseFeedState::Error("boom".to_string()) ); match browse_feed_state(false, Some(&Ok(thing_card_fixtures()))) { BrowseFeedState::Populated(things) => assert_eq!(things.len(), 3), state => panic!("expected populated state, got {state:?}"), } }
#[test] fn hero_shows_only_for_anonymous_and_undismissed() { use crate::session::AuthenticatedIdentity; use jacquard_common::types::string::{Did, Handle};
assert!(should_show_hero(&SessionIdentity::Anonymous, None, false)); assert!(should_show_hero( &SessionIdentity::Anonymous, Some(&Ok(false)), false )); // Either dismissal signal suppresses the band: the SSR seed... assert!(!should_show_hero( &SessionIdentity::Anonymous, Some(&Ok(true)), false )); // ...or the live client-read cookie (e.g. after client-side navigation). assert!(!should_show_hero( &SessionIdentity::Anonymous, Some(&Ok(false)), true )); // An unresolved session must not flash the band before identity lands. assert!(!should_show_hero(&SessionIdentity::Unknown, None, false));
let authed = SessionIdentity::Authenticated(AuthenticatedIdentity { avatar: None, did: Did::new_owned("did:plc:tester").unwrap(), handle: Handle::new_owned("tester.test").unwrap(), display_name: None, }); assert!(!should_show_hero(&authed, None, false)); }
#[test] fn cookie_flag_detects_presence_only_among_other_cookies() { assert!(cookie_has_flag("pm_hero_dismissed=1", HERO_DISMISS_COOKIE)); assert!(cookie_has_flag( "session=abc; pm_hero_dismissed=1; theme=dark", HERO_DISMISS_COOKIE )); assert!(!cookie_has_flag( "session=abc; theme=dark", HERO_DISMISS_COOKIE )); assert!(!cookie_has_flag("", HERO_DISMISS_COOKIE)); // Must not match a cookie whose name merely contains the flag. assert!(!cookie_has_flag( "not_pm_hero_dismissed=1", HERO_DISMISS_COOKIE )); }}