Skip to main content

dioxus_mdx/components/openapi/
endpoint_card.rs

1//! Endpoint card component for displaying a single API operation.
2
3use dioxus::prelude::*;
4use dioxus_free_icons::{Icon, icons::ld_icons::*};
5
6use crate::parser::ApiOperation;
7
8use super::method_badge::MethodBadge;
9use super::parameters_list::ParametersList;
10use super::request_body::RequestBodySection;
11use super::responses_list::ResponsesList;
12
13/// Props for EndpointCard component.
14#[derive(Props, Clone, PartialEq)]
15pub struct EndpointCardProps {
16    /// The operation to display.
17    pub operation: ApiOperation,
18}
19
20/// Collapsible card for an API endpoint.
21#[component]
22pub fn EndpointCard(props: EndpointCardProps) -> Element {
23    let mut is_expanded = use_signal(|| false);
24    let op = &props.operation;
25
26    rsx! {
27        div { class: "border border-base-300 rounded-lg overflow-hidden my-3",
28            // Header - always visible
29            button {
30                class: "w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-base-200/50 transition-colors",
31                onclick: move |_| is_expanded.set(!is_expanded()),
32
33                // Expand/collapse chevron
34                Icon {
35                    class: if is_expanded() { "size-4 text-base-content/50 transform rotate-90 transition-transform shrink-0" } else { "size-4 text-base-content/50 transition-transform shrink-0" },
36                    icon: LdChevronRight
37                }
38
39                // Method badge
40                MethodBadge { method: op.method }
41
42                // Path
43                code { class: "font-mono text-sm text-base-content",
44                    "{op.path}"
45                }
46
47                // Deprecated indicator
48                if op.deprecated {
49                    span { class: "badge badge-warning badge-sm",
50                        "deprecated"
51                    }
52                }
53
54                // Summary (truncated)
55                if let Some(summary) = &op.summary {
56                    span { class: "text-sm text-base-content/60 truncate ml-auto max-w-[40%]",
57                        "{summary}"
58                    }
59                }
60            }
61
62            // Expanded content
63            if is_expanded() {
64                div { class: "border-t border-base-300",
65                    // Summary and description
66                    div { class: "px-4 py-3 bg-base-200/30",
67                        if let Some(summary) = &op.summary {
68                            h4 { class: "font-semibold text-base-content",
69                                "{summary}"
70                            }
71                        }
72                        if let Some(desc) = &op.description {
73                            p { class: "mt-2 text-sm text-base-content/70",
74                                "{desc}"
75                            }
76                        }
77
78                        // Operation ID
79                        if let Some(op_id) = &op.operation_id {
80                            div { class: "mt-2",
81                                span { class: "text-xs text-base-content/50", "Operation ID: " }
82                                code { class: "text-xs font-mono text-base-content/70",
83                                    "{op_id}"
84                                }
85                            }
86                        }
87                    }
88
89                    // Parameters section
90                    if !op.parameters.is_empty() {
91                        div { class: "px-4 py-3 border-t border-base-300",
92                            h5 { class: "text-sm font-semibold text-base-content/80 mb-3 flex items-center gap-2",
93                                Icon { class: "size-4", icon: LdSettings2 }
94                                "Parameters"
95                            }
96                            ParametersList { parameters: op.parameters.clone() }
97                        }
98                    }
99
100                    // Request body section
101                    if let Some(body) = &op.request_body {
102                        div { class: "px-4 py-3 border-t border-base-300",
103                            h5 { class: "text-sm font-semibold text-base-content/80 mb-3 flex items-center gap-2",
104                                Icon { class: "size-4", icon: LdUpload }
105                                "Request Body"
106                            }
107                            RequestBodySection { body: body.clone() }
108                        }
109                    }
110
111                    // Responses section
112                    if !op.responses.is_empty() {
113                        div { class: "px-4 py-3 border-t border-base-300",
114                            h5 { class: "text-sm font-semibold text-base-content/80 mb-3 flex items-center gap-2",
115                                Icon { class: "size-4", icon: LdDownload }
116                                "Responses"
117                            }
118                            ResponsesList { responses: op.responses.clone() }
119                        }
120                    }
121                }
122            }
123        }
124    }
125}