Skip to main content

dioxus_mdx/components/openapi/
endpoint_page.rs

1//! Two-column Mintlify-style endpoint page component.
2
3use dioxus::prelude::*;
4
5use crate::parser::{ApiOperation, OpenApiSpec, highlight_code};
6
7use super::method_badge::MethodBadge;
8use super::parameters_list::ParametersList;
9use super::request_body::RequestBodySection;
10use super::responses_list::ResponsesList;
11
12/// Props for EndpointPage component.
13#[derive(Props, Clone, PartialEq)]
14pub struct EndpointPageProps {
15    /// The operation to display.
16    pub operation: ApiOperation,
17    /// The full OpenAPI spec (for base URL).
18    pub spec: OpenApiSpec,
19}
20
21/// Full-page two-column layout for a single API endpoint.
22///
23/// Left column: method badge, path, summary, description, parameters, request body, responses.
24/// Right column (sticky): curl example, response JSON example.
25#[component]
26pub fn EndpointPage(props: EndpointPageProps) -> Element {
27    let op = &props.operation;
28    let spec = &props.spec;
29
30    let base_url = spec
31        .servers
32        .first()
33        .map(|s| s.url.as_str())
34        .unwrap_or("https://api.example.com");
35
36    let curl = op.generate_curl(base_url);
37    let curl_highlighted = highlight_code(&curl, Some("bash"));
38
39    let response_example = op.generate_response_example();
40
41    let method_bg = op.method.bg_class();
42
43    rsx! {
44        div { class: "flex flex-col lg:flex-row gap-0",
45            // Left column — scrollable content
46            div { class: "flex-1 min-w-0 px-8 py-12 lg:px-12",
47                div { class: "max-w-2xl",
48                    // Method + Path header
49                    div { class: "flex items-center gap-3 mb-6",
50                        span {
51                            class: "px-3 py-1.5 rounded-lg font-mono text-sm font-bold border {method_bg}",
52                            "{op.method.as_str()}"
53                        }
54                        code { class: "font-mono text-lg text-base-content",
55                            "{op.path}"
56                        }
57                        if op.deprecated {
58                            span { class: "badge badge-warning badge-sm", "deprecated" }
59                        }
60                    }
61
62                    // Summary as heading
63                    if let Some(summary) = &op.summary {
64                        h1 { class: "text-3xl font-bold tracking-tight mb-3",
65                            "{summary}"
66                        }
67                    }
68
69                    // Description
70                    if let Some(desc) = &op.description {
71                        p { class: "text-base text-base-content/70 mb-6 leading-relaxed",
72                            "{desc}"
73                        }
74                    }
75
76                    // Base URL
77                    div { class: "mb-8 flex items-center gap-2",
78                        span { class: "text-xs text-base-content/50 font-semibold uppercase tracking-wider",
79                            "Base URL"
80                        }
81                        code { class: "text-sm font-mono text-base-content/70 bg-base-200 px-2 py-1 rounded",
82                            "{base_url}"
83                        }
84                    }
85
86                    // Parameters section
87                    if !op.parameters.is_empty() {
88                        div { class: "mb-8",
89                            h2 { class: "text-lg font-semibold mb-4 pb-2 border-b border-base-300",
90                                "Parameters"
91                            }
92                            ParametersList { parameters: op.parameters.clone() }
93                        }
94                    }
95
96                    // Request Body section
97                    if let Some(body) = &op.request_body {
98                        div { class: "mb-8",
99                            h2 { class: "text-lg font-semibold mb-4 pb-2 border-b border-base-300",
100                                "Request Body"
101                            }
102                            RequestBodySection { body: body.clone() }
103                        }
104                    }
105
106                    // Responses section
107                    if !op.responses.is_empty() {
108                        div { class: "mb-8",
109                            h2 { class: "text-lg font-semibold mb-4 pb-2 border-b border-base-300",
110                                "Responses"
111                            }
112                            ResponsesList { responses: op.responses.clone() }
113                        }
114                    }
115                }
116            }
117
118            // Right column — sticky code examples
119            aside { class: "lg:w-[45%] lg:shrink-0 lg:border-l border-base-300 bg-base-200/20",
120                div { class: "lg:sticky lg:top-16 lg:h-[calc(100vh-4rem)] lg:overflow-y-auto p-6 space-y-6",
121                    // Request example
122                    div {
123                        h3 { class: "text-sm font-semibold text-base-content/70 uppercase tracking-wider mb-3",
124                            "Request"
125                        }
126                        div { class: "rounded-lg border border-base-300 overflow-hidden",
127                            div { class: "px-3 py-2 bg-base-300/50 border-b border-base-300 flex items-center gap-2",
128                                MethodBadge { method: op.method }
129                                code { class: "text-xs font-mono text-base-content/70 truncate",
130                                    "{op.path}"
131                                }
132                            }
133                            pre { class: "bg-base-300/30 p-4 overflow-x-auto syntax-highlight",
134                                code {
135                                    class: "text-sm font-mono leading-relaxed",
136                                    dangerous_inner_html: "{curl_highlighted}",
137                                }
138                            }
139                        }
140                    }
141
142                    // Response example
143                    if let Some((status_code, response_json)) = &response_example {
144                        {
145                            let json_highlighted = highlight_code(response_json, Some("json"));
146                            let status_color = if status_code.starts_with('2') {
147                                "badge-success"
148                            } else if status_code.starts_with('3') {
149                                "badge-info"
150                            } else {
151                                "badge-ghost"
152                            };
153                            rsx! {
154                                div {
155                                    h3 { class: "text-sm font-semibold text-base-content/70 uppercase tracking-wider mb-3",
156                                        "Response"
157                                    }
158                                    div { class: "rounded-lg border border-base-300 overflow-hidden",
159                                        div { class: "px-3 py-2 bg-base-300/50 border-b border-base-300 flex items-center gap-2",
160                                            span { class: "badge {status_color} badge-sm font-mono font-bold",
161                                                "{status_code}"
162                                            }
163                                            span { class: "text-xs text-base-content/50",
164                                                "application/json"
165                                            }
166                                        }
167                                        pre { class: "bg-base-300/30 p-4 overflow-x-auto syntax-highlight max-h-[60vh]",
168                                            code {
169                                                class: "text-sm font-mono leading-relaxed",
170                                                dangerous_inner_html: "{json_highlighted}",
171                                            }
172                                        }
173                                    }
174                                }
175                            }
176                        }
177                    }
178                }
179            }
180        }
181    }
182}