dioxus_mdx/components/
card.rs1use dioxus::prelude::*;
4
5use crate::components::MdxIcon;
6use crate::parser::{CardGroupNode, CardNode};
7
8#[derive(Props, Clone, PartialEq)]
10pub struct DocCardGroupProps {
11 pub group: CardGroupNode,
13 #[props(optional)]
16 pub on_link: Option<EventHandler<String>>,
17 #[props(default = "/docs".to_string())]
19 pub doc_base_path: String,
20}
21
22#[component]
24pub fn DocCardGroup(props: DocCardGroupProps) -> Element {
25 let grid_class = match props.group.cols {
26 1 => "grid grid-cols-1 gap-4",
27 2 => "grid grid-cols-1 md:grid-cols-2 gap-4",
28 3 => "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
29 _ => "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4",
30 };
31
32 rsx! {
33 div { class: "my-6 {grid_class}",
34 for (i, card) in props.group.cards.iter().enumerate() {
35 DocCard {
36 key: "{i}",
37 card: card.clone(),
38 on_link: props.on_link,
39 doc_base_path: props.doc_base_path.clone(),
40 }
41 }
42 }
43 }
44}
45
46#[derive(Props, Clone, PartialEq)]
48pub struct DocCardProps {
49 pub card: CardNode,
51 #[props(optional)]
53 pub on_link: Option<EventHandler<String>>,
54 #[props(default = "/docs".to_string())]
56 pub doc_base_path: String,
57}
58
59#[component]
61pub fn DocCard(props: DocCardProps) -> Element {
62 let html = if !props.card.content.is_empty() {
64 markdown::to_html_with_options(&props.card.content, &markdown::Options::gfm())
65 .unwrap_or_else(|_| props.card.content.clone())
66 } else {
67 String::new()
68 };
69
70 let card_content = rsx! {
71 div { class: "bg-base-300 hover:border-primary/50 transition-colors duration-150 border border-base-content/10 rounded-lg h-full",
72 div { class: "p-6",
73 if let Some(icon) = &props.card.icon {
75 div { class: "text-primary mb-5",
76 MdxIcon { name: icon.clone(), class: "size-6".to_string() }
77 }
78 }
79 h3 { class: "font-semibold text-base-content mb-2 no-underline",
81 "{props.card.title}"
82 }
83 if !html.is_empty() {
85 div {
86 class: "text-sm text-base-content/60 leading-relaxed [&>p]:my-0 [&_a]:no-underline [&_a]:text-base-content/60",
87 dangerous_inner_html: html,
88 }
89 }
90 }
91 }
92 };
93
94 if let Some(href) = &props.card.href {
96 if href.starts_with("http://") || href.starts_with("https://") {
98 rsx! {
99 a {
100 href: "{href}",
101 target: "_blank",
102 rel: "noopener noreferrer",
103 class: "block no-underline hover:no-underline not-prose",
104 {card_content}
105 }
106 }
107 } else {
108 let internal_href = convert_doc_href(href, &props.doc_base_path);
110
111 if let Some(on_link) = &props.on_link {
112 let href_for_click = internal_href.clone();
113 let on_link = *on_link;
114 rsx! {
115 button {
116 class: "block no-underline text-left w-full not-prose",
117 onclick: move |_| on_link.call(href_for_click.clone()),
118 {card_content}
119 }
120 }
121 } else {
122 rsx! {
123 a {
124 href: "{internal_href}",
125 class: "block no-underline hover:no-underline not-prose",
126 {card_content}
127 }
128 }
129 }
130 }
131 } else {
132 card_content
133 }
134}
135
136fn convert_doc_href(href: &str, base_path: &str) -> String {
138 if href.starts_with(base_path) {
140 return href.to_string();
141 }
142 let path = href.trim_start_matches('/');
144 format!("{}/{}", base_path, path)
145}