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}