Skip to main content

dx_utils/
lib.rs

1//! Utility components and functions for [Dioxus](https://dioxuslabs.com/) fullstack apps.
2//!
3//! # Features
4//!
5//! - `server` — enables server-side functionality (HTTP headers via `FullstackContext`).
6//!   This feature is typically activated by your app's `server` feature.
7//!
8//! # Functions
9//!
10//! - [`redirect_external`] — redirect to an external URL, works correctly during
11//!   both SSR and client-side navigation.
12//!
13//! # Components
14//!
15//! - [`LocalTime`] — renders an RFC 3339 datetime in the browser's local timezone.
16//!   Shows UTC during SSR; converts client-side after hydration via `js_sys::Date`.
17
18use dioxus::prelude::*;
19
20// ---------------------------------------------------------------------------
21// redirect_external
22// ---------------------------------------------------------------------------
23
24/// Redirect to an external URL. Works correctly during both SSR (server-side
25/// rendering) and client-side navigation.
26///
27/// **During SSR**: sets HTTP 302 (Found) status and a `Location` header on the
28/// response via [`FullstackContext`], causing a real HTTP redirect before any
29/// HTML reaches the browser.
30///
31/// **On the client** (post-hydration): uses [`navigator().replace()`] with
32/// [`NavigationTarget::External`] for a client-side navigation.
33///
34/// # Arguments
35///
36/// * `url` - The external URL to redirect to.
37///
38/// # Example
39///
40/// ```rust,ignore
41/// use dioxus::prelude::*;
42/// use dx_utils::redirect_external;
43///
44/// #[component]
45/// fn MyGuard() -> Element {
46///     let auth = use_server_future(|| api::check_auth())?;
47///     let binding = auth.read();
48///     let status = binding.as_ref().and_then(|r| r.as_ref().ok());
49///
50///     if let Some(s) = status {
51///         if !s.authenticated {
52///             redirect_external(&s.login_url);
53///             return rsx! {};
54///         }
55///     }
56///
57///     rsx! { Outlet::<Route> {} }
58/// }
59/// ```
60pub fn redirect_external(url: &str) {
61    #[cfg(feature = "server")]
62    {
63        use dioxus::fullstack::FullstackContext;
64        if let Ok(header_value) = http::HeaderValue::from_str(url) {
65            FullstackContext::commit_http_status(http::StatusCode::FOUND, None);
66            if let Some(ctx) = FullstackContext::current() {
67                ctx.add_response_header(http::header::LOCATION, header_value);
68            }
69        }
70    }
71    #[cfg(not(feature = "server"))]
72    {
73        navigator().replace(NavigationTarget::<String>::External(url.to_string()));
74    }
75}
76
77// ---------------------------------------------------------------------------
78// LocalTime component
79// ---------------------------------------------------------------------------
80
81/// Format an RFC 3339 datetime as `YYYY-MM-DD HH:MM` in UTC (server/fallback).
82fn format_utc(iso: &str) -> String {
83    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(iso) {
84        dt.format("%Y-%m-%d %H:%M").to_string()
85    } else {
86        iso.to_string()
87    }
88}
89
90/// Format an RFC 3339 datetime as `YYYY-MM-DD HH:MM` in browser-local time.
91#[cfg(target_arch = "wasm32")]
92fn format_local(iso: &str) -> String {
93    let d = js_sys::Date::new(&iso.into());
94    if d.get_time().is_nan() {
95        return iso.to_string();
96    }
97    let y = d.get_full_year();
98    let m = d.get_month() + 1; // 0-indexed
99    let day = d.get_date();
100    let h = d.get_hours();
101    let min = d.get_minutes();
102    format!("{y:04}-{m:02}-{day:02} {h:02}:{min:02}")
103}
104
105/// Renders an RFC 3339 datetime as a `<time>` element that displays in the
106/// browser's local timezone after hydration. During SSR, shows UTC.
107///
108/// Uses [`js_sys::Date`] on the client to convert to local time — no JavaScript
109/// eval or global scripts needed.
110///
111/// # Props
112///
113/// * `datetime` — an RFC 3339 string (e.g. `"2025-06-15T14:30:00Z"`).
114/// * `class` — optional CSS class for the `<time>` element.
115///
116/// # Example
117///
118/// ```rust,ignore
119/// use dioxus::prelude::*;
120/// use dx_utils::LocalTime;
121///
122/// #[component]
123/// fn UsageRow(hour_bucket: String) -> Element {
124///     rsx! {
125///         td { LocalTime { datetime: hour_bucket } }
126///     }
127/// }
128/// ```
129#[component]
130pub fn LocalTime(datetime: String, #[props(default = "".to_string())] class: String) -> Element {
131    #[allow(unused_mut)]
132    let mut display = use_signal(|| format_utc(&datetime));
133
134    #[cfg(target_arch = "wasm32")]
135    {
136        let dt = datetime.clone();
137        use_effect(move || {
138            display.set(format_local(&dt));
139        });
140    }
141
142    rsx! {
143        time { datetime: "{datetime}", class: "{class}", "{display}" }
144    }
145}