Skip to main content

dioxus_mdx/components/openapi/
endpoint_page.rs

1//! Two-column Mintlify-style endpoint page component.
2
3use dioxus::prelude::*;
4#[cfg(feature = "highlight")]
5use dioxus_code::{Code, CodeTheme, Language, SourceCode, Theme};
6
7use crate::parser::{ApiOperation, OpenApiSpec};
8
9use super::method_badge::MethodBadge;
10use super::parameters_list::ParametersList;
11use super::request_body::RequestBodySection;
12use super::responses_list::ResponsesList;
13
14/// Language selector for the endpoint page's code samples, kept independent of
15/// `dioxus-code` so the markup compiles with the `highlight` feature disabled.
16enum SampleLang {
17    Bash,
18    Json,
19}
20
21/// Render a syntax-highlighted code sample using the fixed GitHub Light / Tokyo Night
22/// system theme (endpoint pages don't follow the site theme toggle).
23#[cfg(feature = "highlight")]
24fn code_sample(code: String, lang: SampleLang) -> Element {
25    let language = match lang {
26        SampleLang::Bash => Language::Bash,
27        SampleLang::Json => Language::Json,
28    };
29    let theme = CodeTheme::system(Theme::GITHUB_LIGHT, Theme::TOKYO_NIGHT);
30    rsx! {
31        Code {
32            src: SourceCode::new(language, code),
33            theme,
34        }
35    }
36}
37
38/// Fallback code sample when the `highlight` feature is disabled: escaped plain text
39/// in the same `<pre class="dxc">` markup, without token coloring.
40#[cfg(not(feature = "highlight"))]
41fn code_sample(code: String, _lang: SampleLang) -> Element {
42    crate::components::code::plain_code_block(&code)
43}
44
45/// Props for EndpointPage component.
46#[derive(Props, Clone, PartialEq)]
47pub struct EndpointPageProps {
48    /// The operation to display.
49    pub operation: ApiOperation,
50    /// The full OpenAPI spec (for base URL).
51    pub spec: OpenApiSpec,
52}
53
54/// Full-page two-column layout for a single API endpoint.
55///
56/// Left column: method badge, path, summary, description, parameters, request body, responses.
57/// Right column (sticky): curl example, response JSON example.
58#[component]
59pub fn EndpointPage(props: EndpointPageProps) -> Element {
60    let op = &props.operation;
61    let spec = &props.spec;
62
63    let base_url = spec
64        .servers
65        .first()
66        .map(|s| s.url.as_str())
67        .unwrap_or("https://api.example.com");
68
69    let curl = op.generate_curl(base_url);
70    let response_example = op.generate_response_example();
71
72    let method_bg = op.method.bg_class();
73
74    rsx! {
75        div { class: "flex flex-col lg:flex-row gap-0",
76            // Left column — scrollable content
77            div { class: "flex-1 min-w-0 px-8 py-12 lg:px-12",
78                div { class: "max-w-2xl",
79                    // Method + Path header
80                    div { class: "flex items-center gap-3 mb-6",
81                        span {
82                            class: "px-3 py-1.5 rounded-lg font-mono text-sm font-bold border {method_bg}",
83                            "{op.method.as_str()}"
84                        }
85                        code { class: "font-mono text-lg text-base-content",
86                            "{op.path}"
87                        }
88                        if op.deprecated {
89                            span { class: "badge badge-warning badge-sm", "deprecated" }
90                        }
91                    }
92
93                    // Summary as heading
94                    if let Some(summary) = &op.summary {
95                        h1 { class: "text-3xl font-bold tracking-tight mb-3",
96                            "{summary}"
97                        }
98                    }
99
100                    // Description
101                    if let Some(desc) = &op.description {
102                        p { class: "text-base text-base-content/70 mb-6 leading-relaxed",
103                            "{desc}"
104                        }
105                    }
106
107                    // Base URL
108                    div { class: "mb-8 flex items-center gap-2",
109                        span { class: "text-xs text-base-content/50 font-semibold uppercase tracking-wider",
110                            "Base URL"
111                        }
112                        code { class: "text-sm font-mono text-base-content/70 bg-base-200 px-2 py-1 rounded",
113                            "{base_url}"
114                        }
115                    }
116
117                    // Parameters section
118                    if !op.parameters.is_empty() {
119                        div { class: "mb-8",
120                            h2 { class: "text-lg font-semibold mb-4 pb-2 border-b border-base-300",
121                                "Parameters"
122                            }
123                            ParametersList { parameters: op.parameters.clone() }
124                        }
125                    }
126
127                    // Request Body section
128                    if let Some(body) = &op.request_body {
129                        div { class: "mb-8",
130                            h2 { class: "text-lg font-semibold mb-4 pb-2 border-b border-base-300",
131                                "Request Body"
132                            }
133                            RequestBodySection { body: body.clone() }
134                        }
135                    }
136
137                    // Responses section
138                    if !op.responses.is_empty() {
139                        div { class: "mb-8",
140                            h2 { class: "text-lg font-semibold mb-4 pb-2 border-b border-base-300",
141                                "Responses"
142                            }
143                            ResponsesList { responses: op.responses.clone() }
144                        }
145                    }
146                }
147            }
148
149            // Right column — sticky code examples
150            aside { class: "lg:w-[45%] lg:shrink-0 lg:border-l border-base-300 bg-base-200/20",
151                div { class: "lg:sticky lg:top-16 lg:h-[calc(100vh-4rem)] lg:overflow-y-auto p-6 space-y-6",
152                    // Request example
153                    div {
154                        h3 { class: "text-sm font-semibold text-base-content/70 uppercase tracking-wider mb-3",
155                            "Request"
156                        }
157                        div { class: "rounded-lg border border-base-300 overflow-hidden",
158                            div { class: "px-3 py-2 bg-base-300/50 border-b border-base-300 flex items-center gap-2",
159                                MethodBadge { method: op.method }
160                                code { class: "text-xs font-mono text-base-content/70 truncate",
161                                    "{op.path}"
162                                }
163                            }
164                            div { class: "dk-code-block-body bg-base-200",
165                                {code_sample(curl.clone(), SampleLang::Bash)}
166                            }
167                        }
168                    }
169
170                    // Response example
171                    if let Some((status_code, response_json)) = &response_example {
172                        {
173                            let status_color = if status_code.starts_with('2') {
174                                "badge-success"
175                            } else if status_code.starts_with('3') {
176                                "badge-info"
177                            } else {
178                                "badge-ghost"
179                            };
180                            rsx! {
181                                div {
182                                    h3 { class: "text-sm font-semibold text-base-content/70 uppercase tracking-wider mb-3",
183                                        "Response"
184                                    }
185                                    div { class: "rounded-lg border border-base-300 overflow-hidden",
186                                        div { class: "px-3 py-2 bg-base-300/50 border-b border-base-300 flex items-center gap-2",
187                                            span { class: "badge {status_color} badge-sm font-mono font-bold",
188                                                "{status_code}"
189                                            }
190                                            span { class: "text-xs text-base-content/50",
191                                                "application/json"
192                                            }
193                                        }
194                                        div { class: "dk-code-block-body bg-base-200 max-h-[60vh] overflow-y-auto",
195                                            {code_sample(response_json.clone(), SampleLang::Json)}
196                                        }
197                                    }
198                                }
199                            }
200                        }
201                    }
202                }
203            }
204        }
205    }
206}