1 From 8791f8574b3631fdf79830dbde85ad517bef6fee Mon Sep 17 00:00:00 20012 From: gl0w <gl0w@world>3 Date: Thu, 10 Sep 2026 06:51:07 +01004 Subject: [PATCH] m6/submit-buttons: + links on issues/pulls + form intros (1185 tests)6 7 ---8 src/forge.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++--9 src/inbox.rs | 48 +++++++++++++++++++++++++++++++++---10 static/style.css | 8 ++++++11 3 files changed, 115 insertions(+), 5 deletions(-)12 13 diff --git a/src/forge.rs b/src/forge.rs14 index 61a34a7..951136b 10064415 --- a/src/forge.rs16 +++ b/src/forge.rs17 @@ -948,8 +948,9 @@ fn render_issues_index(18 description: Option<&str>,19 ) -> Result<String, String> {20 let mut body = String::from("<h1>Issues</h1>\n");21 + body.push_str("<p><a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a></p>\n");22 if data.issues.is_empty() {23 - body.push_str("<p class=\"muted\">No issues yet.</p>\n");24 + body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");25 } else {26 body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27 for issue in &data.issues {28 @@ -994,8 +995,9 @@ fn render_issues_index(29 30 fn render_pulls_index(prs: &[Pr], site_name: &str, nav: &str, description: Option<&str>) -> String {31 let mut body = String::from("<h1>Pulls</h1>\n");32 + body.push_str("<p><a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a></p>\n");33 if prs.is_empty() {34 - body.push_str("<p class=\"muted\">No pull requests yet.</p>\n");35 + body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");36 } else {37 body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");38 for pr in prs {39 @@ -1391,6 +1393,64 @@ mod tests {40 }41 }42 43 + /// f0rge #25: index pages carry prominent `+` submit buttons (correct44 + /// hrefs, icon + words) and the empty states link to the forms.45 + #[test]46 + fn submit_buttons_on_issues_pulls_indexes() {47 + // Empty forge: buttons + linked empty states.48 + let repo = mktemp("submit-empty");49 + let out = mktemp("submit-empty-out");50 + render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");51 + let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");52 + let pulls = std::fs::read_to_string(out.join("pulls/index.html")).expect("pulls");53 + assert!(54 + issues.contains("<a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a>"),55 + "issues + button"56 + );57 + assert!(58 + pulls.contains("<a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a>"),59 + "pulls + button"60 + );61 + assert!(62 + issues.contains("No issues yet.")63 + && issues.contains("href=\"/new-issue.html\">Suggest one"),64 + "issues empty state links to form"65 + );66 + assert!(67 + pulls.contains("No pull requests yet.")68 + && pulls.contains("href=\"/new-pr.html\">Suggest one"),69 + "pulls empty state links to form"70 + );71 + // Non-empty forge: buttons stay above the tables.72 + write_forge(73 + &repo,74 + "issues/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/issue.md",75 + "---\nid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\ntitle: T\nstate: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n",76 + );77 + write_forge(78 + &repo,79 + "prs/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/pr.md",80 + "---\nid: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb\ntitle: P\nstate: open\nbase: main\nhead: fix\ncreated: 2026-09-10T03:00:00Z\n---\ndesc\n",81 + );82 + let out2 = mktemp("submit-full-out");83 + render_loaded(&load(&repo), &out2, "test", "<nav>", None).expect("render");84 + let issues2 = std::fs::read_to_string(out2.join("issues/index.html")).expect("issues");85 + let pulls2 = std::fs::read_to_string(out2.join("pulls/index.html")).expect("pulls");86 + assert!(87 + issues2.contains("href=\"/new-issue.html\">+ New issue"),88 + "issues + button with rows"89 + );90 + assert!(91 + pulls2.contains("href=\"/new-pr.html\">+ New PR prompt"),92 + "pulls + button with rows"93 + );94 + assert!(95 + issues2.contains("table-scroll"),96 + "issues table still renders"97 + );98 + assert!(pulls2.contains("table-scroll"), "pulls table still renders");99 + }100 +101 #[test]102 fn verdict_badge_shapes() {103 assert!(verdict_badge("pass").contains("badge pass"));104 diff --git a/src/inbox.rs b/src/inbox.rs105 index 4998fb0..9806475 100644106 --- a/src/inbox.rs107 +++ b/src/inbox.rs108 @@ -593,7 +593,7 @@ pub fn store_submission(109 // ---- forms (built into dist, zero JS) ----------------------------------110 111 fn license_block() -> String {112 - let mut out = String::from("<blockquote><p>");113 + let mut out = String::from("<blockquote id=\"license-full\"><p>");114 let lines: Vec<&str> = LICENSE_TEXT.split('\n').collect();115 for (i, line) in lines.iter().enumerate() {116 if i > 0 {117 @@ -618,6 +618,9 @@ fn form_page(118 ) -> String {119 let body = format!(120 "<h1>{heading}</h1>\n<p>{intro}</p>\n\121 + <p>What happens: your text goes to a private inbox only the maintainer reads — \122 + never published on this site. No account needed. The server answers a one-time \123 + ack id and shows nothing back.</p>\n\124 <form method=\"post\" action=\"{action}\">\n\125 <p><label for=\"f-title\">Title (required, up to 300 chars)</label><br>\n\126 <input type=\"text\" id=\"f-title\" name=\"title\" maxlength=\"300\" required size=\"60\"></p>\n\127 @@ -631,6 +634,9 @@ fn form_page(128 <p><label>Leave this empty: <input type=\"text\" name=\"website\" value=\"\" autocomplete=\"off\" tabindex=\"-1\"></label></p>\n\129 </div>\n\130 {license}\131 + <p>In plain words: you hand your text over completely — the project can keep, share, \132 + or change it for any purpose, and you keep no rights and are owed nothing. \133 + See the <a href=\"#license-full\">full grant text</a> above.</p>\n\134 <p><label><input type=\"checkbox\" name=\"license\" value=\"agree\" required> \135 I agree to the grant above (required — unchecked submissions are rejected).</label></p>\n\136 <p><button type=\"submit\">Submit</button></p>\n\137 @@ -663,7 +669,7 @@ pub fn render_forms(138 site_name,139 nav,140 "Suggest an issue",141 - "Detailed actionable text from your user. Free textbox on purpose — say what a maintainer can act on.",142 + "Spotted something worth fixing? Write it up here in plain words — say what a maintainer can act on.",143 InboxType::Issue.route(),144 "body",145 "Body",146 @@ -673,7 +679,7 @@ pub fn render_forms(147 site_name,148 nav,149 "Suggest a change",150 - "ONE prompt, plain text. No files, no diff, no branch. Code words inside stay text — never applied, never executed.",151 + "Have an idea for a fix? Send ONE prompt as plain text — no files, no diff, no branch. Code words inside stay text: never applied, never executed.",152 InboxType::PrPrompt.route(),153 "prompt",154 "Prompt",155 @@ -1050,4 +1056,40 @@ mod tests {156 }157 std::fs::remove_dir_all(&dir).ok();158 }159 +160 + /// f0rge #25: submission pages explain the deal in human words161 + /// (private inbox, ack id, no account) plus a plain license summary162 + /// linking to the full grant text — with the full checkbox kept.163 + #[test]164 + fn forms_explain_deal_and_summarize_license() {165 + let dir = std::env::temp_dir().join(format!(166 + "hu13-inbox-forms-deal-{}-{}",167 + std::process::id(),168 + std::time::SystemTime::now()169 + .duration_since(std::time::UNIX_EPOCH)170 + .map(|d| d.as_nanos())171 + .unwrap_or(0)172 + ));173 + std::fs::create_dir_all(&dir).unwrap();174 + render_forms(&dir, "test", "", None).unwrap();175 + let issue = std::fs::read_to_string(dir.join("new-issue.html")).unwrap();176 + let pr = std::fs::read_to_string(dir.join("new-pr.html")).unwrap();177 + for html in [&issue, &pr] {178 + assert!(html.contains("private inbox"), "private suggestion");179 + assert!(html.contains("ack id"), "ack id explained");180 + assert!(html.contains("No account needed"), "no account needed");181 + assert!(html.contains("In plain words:"), "license summary");182 + assert!(183 + html.contains("href=\"#license-full\""),184 + "summary links to full text"185 + );186 + assert!(html.contains("id=\"license-full\""), "full text anchor");187 + assert!(html.contains("name=\"license\""), "checkbox kept");188 + assert!(189 + html.contains("irrevocably give it to this project"),190 + "full grant text kept"191 + );192 + }193 + std::fs::remove_dir_all(&dir).ok();194 + }195 }196 diff --git a/static/style.css b/static/style.css197 index 32f05a8..11a2346 100644198 --- a/static/style.css199 +++ b/static/style.css200 @@ -59,6 +59,14 @@ button,input[type=submit]{display:inline-flex;align-items:center;justify-content201 min-height:40px;min-width:40px;background:#7aa2f7;color:#16161e;border:1px solid #7aa2f7;202 padding:.5rem 1rem;font:inherit;font-weight:700;cursor:pointer}203 button:hover,input[type=submit]:hover{background:#7dcfff}204 +/* Submit buttons (m6/submit-buttons, f0rge #25): link styled as a205 + button — Tokyo Night accent fill, >=40px touch target, icon + words. */206 +.btn-new{display:inline-flex;align-items:center;justify-content:center;207 + min-height:40px;min-width:40px;background:#7aa2f7;color:#16161e;208 + border:1px solid #7aa2f7;padding:.5rem 1rem;font:inherit;font-weight:700;209 + text-decoration:none;cursor:pointer}210 +.btn-new:visited{color:#16161e}211 +.btn-new:hover,.btn-new:focus{background:#7dcfff;text-decoration:none}212 .muted{color:#565f89}213 .badge{display:inline-block;border:1px solid #292e42;padding:.1em .5em;font-size:.85em}214 .badge.pass{color:#9ece6a}.badge.fail{color:#f7768e}215 -- 216 2.55.0217