Skip to main content

reinhardt_admin/pages/components/
common.rs

1//! Common reusable components
2//!
3//! Provides common UI components:
4//! - `Button` - Button component
5//! - `LoadingSpinner` - Loading indicator
6//! - `ErrorDisplay` - Error message display
7//! - `Pagination` - Pagination component
8//! - `SearchBar` - Search input component
9//!
10//! ## Design Note
11//!
12//! These components use PageElement for SSR compatibility and Router integration.
13//! Interactive components with event handlers will be hydrated on the client side.
14
15use reinhardt_pages::Signal;
16use reinhardt_pages::component::{IntoPage, Page, PageElement};
17
18#[cfg(target_arch = "wasm32")]
19use reinhardt_pages::dom::EventType;
20
21#[cfg(target_arch = "wasm32")]
22use std::sync::Arc;
23
24/// Button variant styles
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ButtonVariant {
27	/// Primary action button (blue)
28	Primary,
29	/// Secondary action button (gray)
30	Secondary,
31	/// Success action button (green)
32	Success,
33	/// Danger action button (red)
34	Danger,
35	/// Warning action button (yellow)
36	Warning,
37}
38
39impl ButtonVariant {
40	/// Get CSS class for this variant
41	pub fn class(&self) -> &'static str {
42		match self {
43			ButtonVariant::Primary => "btn-primary",
44			ButtonVariant::Secondary => "btn-secondary",
45			ButtonVariant::Success => "btn-success",
46			ButtonVariant::Danger => "btn-danger",
47			ButtonVariant::Warning => "btn-warning",
48		}
49	}
50}
51
52/// Button component
53///
54/// Displays a styled button with various variants.
55/// When clicked, sets the provided Signal to true.
56///
57/// # Example
58///
59/// ```ignore
60/// use reinhardt_admin::pages::components::common::*;
61/// use reinhardt_pages::Signal;
62///
63/// let clicked = Signal::new(false);
64/// button("Click me", ButtonVariant::Primary, false, clicked)
65/// ```
66pub fn button(text: &str, variant: ButtonVariant, disabled: bool, _on_click: Signal<bool>) -> Page {
67	let classes = format!("btn {}", variant.class());
68
69	#[cfg(target_arch = "wasm32")]
70	let button_view = {
71		PageElement::new("button")
72			.attr("class", classes.clone())
73			.attr("type", "button")
74			.attr("disabled", if disabled { "true" } else { "false" })
75			.on(
76				EventType::Click,
77				Arc::new(move |_event: web_sys::Event| {
78					_on_click.set(true);
79				}),
80			)
81			.child(text.to_string())
82	};
83
84	#[cfg(not(target_arch = "wasm32"))]
85	let button_view = {
86		// SSR: No event handler needed (will be hydrated on client)
87		PageElement::new("button")
88			.attr("class", classes)
89			.attr("type", "button")
90			.attr("disabled", if disabled { "true" } else { "false" })
91			.attr("data-reactive", "true") // Marker for client-side hydration
92			.child(text.to_string())
93	};
94
95	button_view.into_page()
96}
97
98/// Loading spinner component
99///
100/// Displays a loading spinner while data is being fetched.
101///
102/// # Example
103///
104/// ```ignore
105/// use reinhardt_admin::pages::components::common::loading_spinner;
106///
107/// loading_spinner()
108/// ```
109pub fn loading_spinner() -> Page {
110	PageElement::new("div")
111		.attr("class", "loading-spinner")
112		.child(
113			PageElement::new("div")
114				.attr("class", "spinner-border")
115				.attr("role", "status")
116				.child(
117					PageElement::new("span")
118						.attr("class", "visually-hidden")
119						.child("Loading..."),
120				),
121		)
122		.into_page()
123}
124
125/// Error display component
126///
127/// Displays error messages in a styled container.
128///
129/// # Example
130///
131/// ```ignore
132/// use reinhardt_admin::pages::components::common::error_display;
133///
134/// error_display("An error occurred", true)
135/// ```
136pub fn error_display(message: &str, dismissible: bool) -> Page {
137	let mut container = PageElement::new("div")
138		.attr("class", "alert alert-danger")
139		.attr("role", "alert");
140
141	if dismissible {
142		container = container.child(
143			PageElement::new("button")
144				.attr("class", "btn-close")
145				.attr("type", "button")
146				.attr("data-bs-dismiss", "alert")
147				.attr("aria-label", "Close"),
148		);
149	}
150
151	container.child(message.to_string()).into_page()
152}
153
154/// Pagination component
155///
156/// Displays pagination controls for navigating through pages.
157/// Updates the provided Signal when page navigation occurs.
158///
159/// # Example
160///
161/// ```ignore
162/// use reinhardt_admin::pages::components::common::pagination;
163/// use reinhardt_pages::Signal;
164///
165/// let current_page = Signal::new(1u64);
166/// pagination(current_page, 10)
167/// ```
168pub fn pagination(current_page: Signal<u64>, total_pages: u64) -> Page {
169	let current_val = current_page.get();
170	let mut nav_items = Vec::new();
171
172	// Previous button
173	let prev_disabled = current_val <= 1;
174	nav_items.push(create_page_item(
175		"Previous",
176		prev_disabled,
177		false,
178		current_page.clone(),
179		move |page: Signal<u64>| {
180			let current = page.get();
181			if current > 1 {
182				page.set(current - 1);
183			}
184		},
185	));
186
187	// Page numbers (show up to 5 pages around current)
188	let start = current_val.saturating_sub(2).max(1);
189	let end = (current_val + 2).min(total_pages);
190
191	for page_num in start..=end {
192		let is_current = page_num == current_val;
193		let page_num_str = page_num.to_string();
194		nav_items.push(create_page_item(
195			&page_num_str,
196			false,
197			is_current,
198			current_page.clone(),
199			move |page: Signal<u64>| {
200				page.set(page_num);
201			},
202		));
203	}
204
205	// Next button
206	let next_disabled = current_val >= total_pages;
207	nav_items.push(create_page_item(
208		"Next",
209		next_disabled,
210		false,
211		current_page,
212		move |page: Signal<u64>| {
213			let current = page.get();
214			if current < total_pages {
215				page.set(current + 1);
216			}
217		},
218	));
219
220	PageElement::new("div")
221		.attr("class", "d-flex justify-content-center")
222		.child(
223			PageElement::new("ul")
224				.attr("class", "pagination")
225				.children(nav_items),
226		)
227		.into_page()
228}
229
230/// Helper function to create a pagination item with event handler
231fn create_page_item<F>(
232	text: &str,
233	disabled: bool,
234	active: bool,
235	_signal: Signal<u64>,
236	_handler: F,
237) -> Page
238where
239	F: Fn(Signal<u64>) + 'static,
240{
241	let class_name = if active {
242		"page-item active"
243	} else if disabled {
244		"page-item disabled"
245	} else {
246		"page-item"
247	};
248
249	#[cfg(target_arch = "wasm32")]
250	let link = {
251		PageElement::new("a")
252			.attr("class", "page-link")
253			.attr("href", "#")
254			.child(text.to_string())
255			.on(
256				EventType::Click,
257				Arc::new(move |_event: web_sys::Event| {
258					_handler(_signal.clone());
259				}),
260			)
261	};
262
263	#[cfg(not(target_arch = "wasm32"))]
264	let link = {
265		// SSR: No event handler needed (will be hydrated on client)
266		PageElement::new("a")
267			.attr("class", "page-link")
268			.attr("href", "#")
269			.attr("data-reactive", "true") // Marker for client-side hydration
270			.child(text.to_string())
271	};
272
273	PageElement::new("li")
274		.attr("class", class_name)
275		.child(link)
276		.into_page()
277}
278
279/// Search bar component
280///
281/// Displays a search input with icon.
282/// The current value is displayed from the Signal.
283///
284/// Note: Input value updates must be handled via form binding or external mechanisms.
285/// This component only displays the current Signal value.
286///
287/// # Example
288///
289/// ```ignore
290/// use reinhardt_admin::pages::components::common::search_bar;
291/// use reinhardt_pages::Signal;
292///
293/// let search_value = Signal::new(String::new());
294/// search_bar(search_value, "Search...")
295/// ```
296pub fn search_bar(value: Signal<String>, placeholder: &str) -> Page {
297	let current_value = value.get();
298
299	PageElement::new("div")
300		.attr("class", "input-group")
301		.child(
302			PageElement::new("span")
303				.attr("class", "input-group-text")
304				.child(PageElement::new("i").attr("class", "bi bi-search")),
305		)
306		.child(
307			PageElement::new("input")
308				.attr("class", "form-control")
309				.attr("type", "text")
310				.attr("placeholder", placeholder.to_string())
311				.attr("value", current_value),
312		)
313		.into_page()
314}