ironhtml_bootstrap/
cards.rs

1//! Bootstrap card components.
2//!
3//! Provides type-safe Bootstrap cards matching the
4//! [Bootstrap cards documentation](https://getbootstrap.com/docs/5.3/components/card/).
5//!
6//! ## Example
7//!
8//! ```rust
9//! use ironhtml_bootstrap::cards::*;
10//!
11//! // Simple card
12//! let simple = card(|c| c.text("Card content"));
13//! assert!(simple.render().contains(r#"class="card"#));
14//!
15//! // Card with title and text (from Bootstrap docs)
16//! let doc_card = card_simple("Card title", "Some text", "Go", "#");
17//! assert!(doc_card.render().contains("card-title"));
18//! assert!(doc_card.render().contains("card-text"));
19//! ```
20
21use crate::Color;
22use ironhtml::typed::Element;
23use ironhtml_elements::{Div, Img, A, H5, P};
24
25extern crate alloc;
26use alloc::format;
27
28/// Create a basic Bootstrap card.
29///
30/// Generates:
31/// ```html
32/// <div class="card">
33///   <div class="card-body">...</div>
34/// </div>
35/// ```
36///
37/// ## Example
38///
39/// ```rust
40/// use ironhtml_bootstrap::cards::card;
41///
42/// let c = card(|body| body.text("Card content"));
43/// assert!(c.render().contains(r#"class="card"#));
44/// assert!(c.render().contains(r#"class="card-body"#));
45/// ```
46#[must_use]
47pub fn card<F>(f: F) -> Element<Div>
48where
49    F: FnOnce(Element<Div>) -> Element<Div>,
50{
51    Element::<Div>::new()
52        .class("card")
53        .child::<Div, _>(|body| f(body.class("card-body")))
54}
55
56/// Create a card with specific width.
57#[must_use]
58pub fn card_width<F>(width: &str, f: F) -> Element<Div>
59where
60    F: FnOnce(Element<Div>) -> Element<Div>,
61{
62    let style = format!("width: {width};");
63    Element::<Div>::new()
64        .class("card")
65        .attr("style", &style)
66        .child::<Div, _>(|body| f(body.class("card-body")))
67}
68
69/// Create a card with title and text (from Bootstrap docs example).
70///
71/// Generates:
72/// ```html
73/// <div class="card" style="width: 18rem;">
74///   <div class="card-body">
75///     <h5 class="card-title">Card title</h5>
76///     <p class="card-text">Some quick example text...</p>
77///     <a href="#" class="btn btn-primary">Go somewhere</a>
78///   </div>
79/// </div>
80/// ```
81#[must_use]
82pub fn card_simple(title: &str, text: &str, link_text: &str, link_href: &str) -> Element<Div> {
83    Element::<Div>::new()
84        .class("card")
85        .attr("style", "width: 18rem;")
86        .child::<Div, _>(|body| {
87            body.class("card-body")
88                .child::<H5, _>(|h| h.class("card-title").text(title))
89                .child::<P, _>(|p| p.class("card-text").text(text))
90                .child::<A, _>(|a| {
91                    a.attr("href", link_href)
92                        .class("btn btn-primary")
93                        .text(link_text)
94                })
95        })
96}
97
98/// Create a card with image on top (from Bootstrap docs).
99///
100/// Generates:
101/// ```html
102/// <div class="card" style="width: 18rem;">
103///   <img src="..." class="card-img-top" alt="...">
104///   <div class="card-body">
105///     <h5 class="card-title">Card title</h5>
106///     <p class="card-text">Some text...</p>
107///   </div>
108/// </div>
109/// ```
110#[must_use]
111pub fn card_with_image(img_src: &str, img_alt: &str, title: &str, text: &str) -> Element<Div> {
112    Element::<Div>::new()
113        .class("card")
114        .attr("style", "width: 18rem;")
115        .child::<Img, _>(|img| {
116            img.attr("src", img_src)
117                .class("card-img-top")
118                .attr("alt", img_alt)
119        })
120        .child::<Div, _>(|body| {
121            body.class("card-body")
122                .child::<H5, _>(|h| h.class("card-title").text(title))
123                .child::<P, _>(|p| p.class("card-text").text(text))
124        })
125}
126
127/// Create a card with header and footer.
128///
129/// Generates:
130/// ```html
131/// <div class="card">
132///   <div class="card-header">{header}</div>
133///   <div class="card-body">...</div>
134///   <div class="card-footer text-body-secondary">{footer}</div>
135/// </div>
136/// ```
137#[must_use]
138pub fn card_with_header_footer<F>(header: &str, footer: &str, f: F) -> Element<Div>
139where
140    F: FnOnce(Element<Div>) -> Element<Div>,
141{
142    Element::<Div>::new()
143        .class("card")
144        .child::<Div, _>(|h| h.class("card-header").text(header))
145        .child::<Div, _>(|body| f(body.class("card-body")))
146        .child::<Div, _>(|foot| foot.class("card-footer text-body-secondary").text(footer))
147}
148
149/// Create a colored card (text-bg-{color}).
150#[must_use]
151pub fn card_colored<F>(color: Color, f: F) -> Element<Div>
152where
153    F: FnOnce(Element<Div>) -> Element<Div>,
154{
155    let class = format!("card text-bg-{}", color.as_str());
156    Element::<Div>::new()
157        .class(&class)
158        .child::<Div, _>(|body| f(body.class("card-body")))
159}
160
161/// Create a card with border color.
162#[must_use]
163pub fn card_border<F>(color: Color, f: F) -> Element<Div>
164where
165    F: FnOnce(Element<Div>) -> Element<Div>,
166{
167    let class = format!("card border-{}", color.as_str());
168    Element::<Div>::new()
169        .class(&class)
170        .child::<Div, _>(|body| f(body.class("card-body")))
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_basic_card() {
179        let c = card(|b| b.text("Hello"));
180        let html = c.render();
181        assert!(html.contains(r#"class="card"#));
182        assert!(html.contains(r#"class="card-body"#));
183        assert!(html.contains("Hello"));
184    }
185
186    #[test]
187    fn test_card_simple() {
188        let c = card_simple("Title", "Text content", "Click", "#");
189        let html = c.render();
190        assert!(html.contains("card-title"));
191        assert!(html.contains("Title"));
192        assert!(html.contains("card-text"));
193        assert!(html.contains("btn btn-primary"));
194    }
195
196    #[test]
197    fn test_card_with_image() {
198        let c = card_with_image("/img.jpg", "Alt text", "Title", "Description");
199        let html = c.render();
200        assert!(html.contains("card-img-top"));
201        assert!(html.contains(r#"src="/img.jpg"#));
202        assert!(html.contains(r#"alt="Alt text"#));
203    }
204
205    #[test]
206    fn test_card_with_header_footer() {
207        let c = card_with_header_footer("Header", "Footer", |b| b.text("Body"));
208        let html = c.render();
209        assert!(html.contains("card-header"));
210        assert!(html.contains("card-footer"));
211        assert!(html.contains("Header"));
212        assert!(html.contains("Footer"));
213    }
214}