Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197use dioxus::prelude::*;
use crate::Route;use crate::auth::{ToastState, broadcast_sign_out, logout_url};use crate::examples::{random_sample_handle, random_sample_search_query};use crate::session::SessionIdentity;
/// Global product shell. Renders the persistent header (wordmark, primary nav,/// global directed-search field, Publish CTA, and session/account control) once/// and hosts every route through the router `Outlet`. Pages render their own/// `<main>`/`<h1>` inside the outlet, so the shell deliberately uses a `<div>`/// content region rather than a competing `<main>`.#[component]pub(crate) fn AppShell() -> Element { let session = use_context::<Signal<SessionIdentity>>(); let toasts = use_context::<Signal<ToastState>>(); let route = use_route::<Route>(); let show_browse_link = !matches!(route, Route::Browse {});
// One orange (`--color-action`) per view: while logged out the primary // action is Sign in (in `SessionControl`), so Publish drops to secondary // until the visitor is authenticated and can actually publish. `Unknown` // (pre-resolution) stays secondary to avoid an orange flash for visitors // who turn out anonymous. let publish_class = if matches!(&*session.read(), SessionIdentity::Authenticated(_)) { "button button-primary app-publish-cta" } else { "button button-secondary app-publish-cta" };
rsx! { div { class: "app-shell", header { class: "app-header", div { class: "app-header-inner", Link { class: "app-wordmark", to: Route::Browse {}, aria_label: "Polymodel home", "Polymodel" } nav { class: "app-nav", aria_label: "Primary", if show_browse_link { Link { class: "app-nav-link", active_class: "is-active", to: Route::Browse {}, "Browse" } } } ShellSearch {} Link { class: publish_class, to: Route::Publish { draft: String::new() }, "Publish" } SessionControl { session, toasts } } } div { class: "app-content", Outlet::<Route> {} } } }}
/// Global directed-search field. Uses a plain GET form so SSR/pre-hydration/// submissions still resolve to `/search?q=...`; Dioxus keeps the value synced/// with the current route once hydrated.#[component]fn ShellSearch() -> Element { let route = use_route::<Route>(); let route_query = match route { Route::Search { q } => Some(q), _ => None, }; let mut query = use_signal(|| route_query.clone().unwrap_or_default()); let mut synced_route_query = use_signal(|| route_query.clone()); let search_placeholder = use_server_cached(random_sample_search_query);
if *synced_route_query.read() != route_query { synced_route_query.set(route_query.clone()); if let Some(route_query) = route_query { query.set(route_query); } }
rsx! { form { class: "app-search", role: "search", action: "/search", method: "get", input { class: "app-search-input", r#type: "search", name: "q", placeholder: "{search_placeholder}", aria_label: "Search Polymodel", value: "{query}", oninput: move |event| query.set(event.value()), } button { class: "button button-secondary app-search-button", r#type: "submit", "Search" } } }}
/// Returns the current route as a path string for use as an OAuth `return_to`/// value, so signing in returns the user to the page they started on.fn current_return_to() -> String { use_route::<Route>().to_string()}
#[allow(clippy::useless_format)]#[component]pub(crate) fn SessionControl( session: Signal<SessionIdentity>, toasts: Signal<ToastState>,) -> Element { let identity = session.read().clone(); let return_to = current_return_to(); let handle_placeholder = use_server_cached(random_sample_handle); let mut identifier = use_signal(String::new);
match identity { // Session still resolving: render nothing rather than flashing a sign-in // form at users who turn out to be authenticated. SessionIdentity::Unknown => rsx! {}, SessionIdentity::Anonymous => rsx! { form { class: "session-control", action: "/oauth/start", method: "get", input { r#type: "hidden", name: "return_to", value: "{return_to}" }, input { r#type: "text", name: "identifier", placeholder: "{handle_placeholder}", aria_label: "ATProto handle", value: "{identifier}", oninput: move |event| identifier.set(event.value()), } button { class: "button button-primary", r#type: "submit", "Sign in" } } }, SessionIdentity::Authenticated(identity) => { let label = identity.label().to_string(); let handle = identity.handle.as_str().to_string(); let avatar_url = identity.avatar.as_ref().map(|u| u.as_ref().to_string()); let initial = label.chars().next().unwrap_or('?').to_ascii_uppercase(); rsx! { details { class: "account-menu", summary { class: "account-trigger", aria_label: "Account menu for {label}", if let Some(url) = &avatar_url { img { class: "session-avatar", src: "{url}", alt: "{label}", loading: "lazy" } } else { span { class: "session-avatar session-avatar-fallback", aria_hidden: "true", "{initial}" } } } div { class: "account-menu-panel blueprint-panel", div { class: "account-menu-identity", strong { "{label}" } span { class: "account-menu-handle", "@{handle}" } } Link { class: "account-menu-item", to: Route::ProfileSelf {}, "Profile" } form { class: "account-menu-form", method: "post", action: "{logout_url()}", button { class: "account-menu-item account-menu-signout", r#type: "submit", onclick: move |_| { broadcast_sign_out(); session.set(SessionIdentity::Anonymous); toasts.write().push("Signing out…"); }, "Sign out" } } } } } } }}
#[allow(clippy::useless_format)]#[component]pub(crate) fn ToastShelf(toasts: Signal<ToastState>) -> Element { let items = toasts.read().items().to_vec();
rsx! { div { class: "toast-shelf", aria_live: "polite", for toast in items { div { key: "toast-{toast.id}", class: "toast blueprint-panel", span { "{toast.message}" } button { class: "toast-dismiss", aria_label: "Dismiss notification", onclick: move |_| toasts.write().dismiss(toast.id), "×" } } } } }}