atproto blogging
1//! Bug report dialog for the markdown editor.
2//!
3//! Captures editor state, DOM, and platform info for bug reports.
4//! All capture happens on-demand when the report button is clicked.
5
6use dioxus::prelude::*;
7
8#[allow(unused_imports)]
9use super::log_buffer;
10#[allow(unused_imports)]
11use super::storage::load_from_storage;
12
13/// Captured report data.
14#[derive(Clone, Default)]
15struct ReportData {
16 editor_text: String,
17 dom_html: String,
18 platform_info: String,
19 recent_logs: String,
20}
21
22impl ReportData {
23 /// Capture current state from DOM and LocalStorage.
24 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
25 fn capture(editor_id: &str) -> Self {
26 let dom_html = web_sys::window()
27 .and_then(|w| w.document())
28 .and_then(|d| d.get_element_by_id(editor_id))
29 .map(|e| e.outer_html())
30 .unwrap_or_default();
31
32 let editor_text = load_from_storage("current")
33 .map(|doc| doc.content())
34 .unwrap_or_default();
35
36 let platform_info = {
37 let plat = weaver_editor_browser::platform();
38 format!(
39 "iOS: {}, Android: {}, Safari: {}, Chrome: {}, Firefox: {}, Mobile: {}\n\
40 User Agent: {}",
41 plat.ios,
42 plat.android,
43 plat.safari,
44 plat.chrome,
45 plat.gecko,
46 plat.mobile,
47 web_sys::window()
48 .and_then(|w| w.navigator().user_agent().ok())
49 .unwrap_or_default()
50 )
51 };
52
53 let recent_logs = log_buffer::get_logs();
54
55 Self {
56 editor_text,
57 dom_html,
58 platform_info,
59 recent_logs,
60 }
61 }
62
63 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
64 fn capture(_editor_id: &str) -> Self {
65 Self::default()
66 }
67
68 /// Generate mailto URL with report data.
69 fn to_mailto(&self, email: &str, comment: &str) -> String {
70 let subject = "Weaver Editor Bug Report";
71
72 let body = format!(
73 "## Bug Report\n\n\
74 ### Comment\n{}\n\n\
75 ### Platform Info\n```\n{}\n```\n\n\
76 ### Recent Logs\n```\n{}\n```\n\n\
77 ### Editor Text\n```markdown\n{}\n```\n\n\
78 ### DOM State\n```html\n{}\n```",
79 comment, self.platform_info, self.recent_logs, self.editor_text, self.dom_html
80 );
81
82 let encoded_subject = urlencoding::encode(subject);
83 let encoded_body = urlencoding::encode(&body);
84
85 format!(
86 "mailto:{}?subject={}&body={}",
87 email, encoded_subject, encoded_body
88 )
89 }
90}
91
92/// Props for the bug report button.
93#[derive(Props, Clone, PartialEq)]
94pub struct ReportButtonProps {
95 /// Email address to send reports to.
96 pub email: String,
97 /// Editor element ID for DOM capture.
98 pub editor_id: String,
99}
100
101/// Bug report button and dialog.
102#[component]
103pub fn ReportButton(props: ReportButtonProps) -> Element {
104 let mut show_dialog = use_signal(|| false);
105 let mut comment = use_signal(String::new);
106 let mut report_data = use_signal(ReportData::default);
107
108 let editor_id = props.editor_id.clone();
109 let capture_state = move |_| {
110 report_data.set(ReportData::capture(&editor_id));
111 show_dialog.set(true);
112 };
113
114 let email = props.email.clone();
115 let submit_report = move |_| {
116 let data = report_data();
117 #[allow(unused_variables)]
118 let mailto_url = data.to_mailto(&email, &comment());
119
120 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
121 if let Some(window) = web_sys::window() {
122 let _ = window.open_with_url(&mailto_url);
123 }
124
125 show_dialog.set(false);
126 comment.set(String::new());
127 };
128
129 let close_dialog = move |_| {
130 show_dialog.set(false);
131 };
132
133 rsx! {
134 button {
135 class: "report-bug-button",
136 onclick: capture_state,
137 "Report Bug"
138 }
139
140 if show_dialog() {
141 div {
142 class: "report-dialog-overlay",
143 role: "dialog",
144 aria_modal: "true",
145 aria_labelledby: "report-dialog-title",
146 onclick: close_dialog,
147
148 div {
149 class: "report-dialog",
150 onclick: move |e| e.stop_propagation(),
151
152 h2 { id: "report-dialog-title", "Report a Bug" }
153
154 div { class: "report-section",
155 label { "Describe the issue:" }
156 textarea {
157 class: "report-comment",
158 aria_label: "Describe the issue",
159 placeholder: "What happened? What did you expect?",
160 value: "{comment}",
161 oninput: move |e| comment.set(e.value()),
162 rows: "4",
163 }
164 }
165
166 details { class: "report-details",
167 summary { "Captured Data (click to expand)" }
168
169 div { class: "report-section",
170 h4 { "Platform" }
171 pre { "{report_data().platform_info}" }
172 }
173
174 div { class: "report-section",
175 h4 { "Recent Logs" }
176 pre { "{report_data().recent_logs}" }
177 }
178
179 div { class: "report-section",
180 h4 { "Editor Text" }
181 pre { "{report_data().editor_text}" }
182 }
183
184 div { class: "report-section",
185 h4 { "DOM HTML" }
186 pre { "{report_data().dom_html}" }
187 }
188 }
189
190 div { class: "report-actions",
191 button {
192 class: "report-cancel",
193 onclick: close_dialog,
194 "Cancel"
195 }
196 button {
197 class: "report-submit",
198 onclick: submit_report,
199 "Open Email"
200 }
201 }
202 }
203 }
204 }
205 }
206}