// All Rights Reserved — The Cochran Block, LLC
// Contributors: Mattbusel (XFactor), GotEmCoach, KOVA, Claude Opus 4.6, SuperNinja, Composer 1.5, Google Gemini Pro 3
// Community Grant: eligibility-audit questionnaire, reuses intake pattern.
// Unlisted page — no nav link.
use axum::Form;
use axum::extract::{ConnectInfo, Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{Html, IntoResponse, Redirect};
use chrono::Utc;
use serde::Deserialize;
use std::net::SocketAddr;
use std::sync::Arc;
use uuid::Uuid;
use super::pages::{C7, C8, f62};
use crate::db::{self, GrantRow};
use crate::t0;
const INTRO: &str = include_str!("../../content/community_grant_intro.txt");
pub(crate) fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
pub(crate) fn client_ip(addr: SocketAddr, headers: &HeaderMap) -> String {
headers
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.split(',').next())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| addr.ip().to_string())
}
/// GET /community-grant — eligibility audit form.
pub async fn get_form(State(_s): State<Arc<t0>>) -> Html<String> {
let intro_escaped = html_escape(INTRO);
let content = format!(
r#"<section class="intake-section"><div class="intake-steps"><span class="intake-step intake-active"><span class="intake-num">1</span> Review</span><span class="intake-step"><span class="intake-num">2</span> Submit</span><span class="intake-step"><span class="intake-num">3</span> Complete</span></div>
<div class="intake-doc"><h1 class="intake-title">The Cochranblock Community Grant Application</h1><p class="intake-intro">{}</p>
<div class="intake-sign"><div class="intake-line"></div><p class="intake-sign-label">Submit your application</p>
<form class="intake-form" method="post" action="/community-grant" id="grant-form">
<label for="org_name">Organization Name</label><input type="text" id="org_name" name="org_name" required autocomplete="organization" placeholder="Your organization name" maxlength="200">
<label for="ein">501(c)(3) EIN or Tax ID (If applicable)</label><input type="text" id="ein" name="ein" autocomplete="off" placeholder="XX-XXXXXXX" maxlength="20">
<label for="contact_name">Point of Contact — Name</label><input type="text" id="contact_name" name="contact_name" required autocomplete="name" placeholder="Full name" maxlength="200">
<label for="contact_email">Point of Contact — Email</label><input type="email" id="contact_email" name="contact_email" required autocomplete="email" placeholder="contact@org.org" maxlength="254">
<label for="mission">The Mission</label><textarea id="mission" name="mission" required placeholder="Briefly describe your organization's primary mission. How specifically does your work serve and elevate the local community?" maxlength="2000"></textarea>
<label for="technical_objective">Technical Objective</label><textarea id="technical_objective" name="technical_objective" required placeholder="What will this web appliance be used for? (e.g., Volunteer intake routing, public information broadcasting, donation processing parameters)" maxlength="2000"></textarea>
<div class="intake-hp" aria-hidden="true"><label for="website_url">Leave blank</label><input type="text" id="website_url" name="website_url" tabindex="-1" autocomplete="off"></div>
<div class="intake-consent"><label class="intake-check"><input type="checkbox" name="consent_grant" value="1" required id="consent_grant"> I acknowledge the hardware requirements and the $500 deployment fee should our application be selected for this quarter's grant.</label></div>
<button type="submit" class="btn" id="grant-submit-btn" disabled>Submit Application</button></form></div>
<p class="intake-note">If awarded, this grant subsidizes 85% of our standard deployment costs. One deployment per fiscal quarter.</p></div></section>
<script>(function(){{var f=document.getElementById('grant-form');var b=document.getElementById('grant-submit-btn');var c=document.getElementById('consent_grant');var o=document.getElementById('org_name');var n=document.getElementById('contact_name');var e=document.getElementById('contact_email');var m=document.getElementById('mission');var t=document.getElementById('technical_objective');function chk(){{var ok=c&&o&&n&&e&&m&&t&&c.checked&&o.value.trim()&&n.value.trim()&&e.value.trim()&&m.value.trim()&&t.value.trim();b.disabled=!ok;}}if(f){{c.onchange=chk;o.oninput=o.onchange=n.oninput=n.onchange=e.oninput=e.onchange=m.oninput=m.onchange=t.oninput=t.onchange=chk;chk();}}}})();</script>"#,
intro_escaped
);
Html(format!(
"{}{}{}{}",
f62("community-grant", "Community Grant | CochranBlock"),
C7,
content,
C8
))
}
#[derive(Deserialize)]
pub struct CommunityGrantForm {
org_name: String,
#[serde(default)]
ein: String,
contact_name: String,
contact_email: String,
mission: String,
technical_objective: String,
#[serde(rename = "website_url")]
honeypot: Option<String>,
consent_grant: Option<String>,
}
pub(crate) fn validate_grant_input(
org_name: &str,
contact_name: &str,
contact_email: &str,
mission: &str,
technical_objective: &str,
) -> Result<(), &'static str> {
if org_name.trim().is_empty() {
return Err("org name empty");
}
if contact_name.trim().is_empty() {
return Err("contact name empty");
}
if contact_email.trim().is_empty() {
return Err("contact email empty");
}
if mission.trim().is_empty() {
return Err("mission empty");
}
if technical_objective.trim().is_empty() {
return Err("technical objective empty");
}
if org_name.len() > 200 || contact_name.len() > 200 || contact_email.len() > 254 {
return Err("field too long");
}
if mission.len() > 2000 || technical_objective.len() > 2000 {
return Err("field too long");
}
Ok(())
}
/// POST /community-grant — process form.
pub async fn post_form(
State(s): State<Arc<t0>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
Form(f): Form<CommunityGrantForm>,
) -> impl IntoResponse {
if f.honeypot
.as_deref()
.map(|v| !v.trim().is_empty())
.unwrap_or(false)
{
tracing::debug!("community grant honeypot triggered");
return (StatusCode::OK, Html(confirmed_html(None))).into_response();
}
let org_name = f.org_name.trim();
let ein = f.ein.trim();
let contact_name = f.contact_name.trim();
let contact_email = f.contact_email.trim();
let mission = f.mission.trim();
let technical_objective = f.technical_objective.trim();
if validate_grant_input(
org_name,
contact_name,
contact_email,
mission,
technical_objective,
)
.is_err()
{
return (StatusCode::BAD_REQUEST, "Invalid input").into_response();
}
if f.consent_grant.as_deref() != Some("1") {
return (
StatusCode::BAD_REQUEST,
"You must acknowledge the hardware requirements and $500 deployment fee.",
)
.into_response();
}
let db = match &s.intake_db {
Some(d) => d.clone(),
None => {
tracing::error!("community grant: db not available");
return (
StatusCode::SERVICE_UNAVAILABLE,
"Service temporarily unavailable. Please try again later.",
)
.into_response();
}
};
let id = Uuid::new_v4().to_string();
let submitted_at = Utc::now().to_rfc3339();
let ip = client_ip(addr, &headers);
let ua = headers
.get("user-agent")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let row = GrantRow {
id: id.clone(),
org_name: org_name.to_string(),
ein: if ein.is_empty() { None } else { Some(ein.to_string()) },
contact_name: contact_name.to_string(),
contact_email: contact_email.to_string(),
mission: mission.to_string(),
technical_objective: technical_objective.to_string(),
submitted_at: submitted_at.clone(),
ip_address: if ip.is_empty() { None } else { Some(ip.clone()) },
user_agent: if ua.is_empty() { None } else { Some(ua.clone()) },
consent_grant: true,
};
let row_clone = row.clone();
let insert_result = tokio::task::spawn_blocking(move || db::f51(&db, &row_clone))
.await
.unwrap_or_else(|e| Err(crate::error::t18::E6(format!("join: {}", e))));
if let Err(e) = insert_result {
tracing::error!("community grant insert failed: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Unable to save. Please try again.",
)
.into_response();
}
if let Some(url) = std::env::var("INTAKE_WEBHOOK_URL")
.ok()
.map(|u| u.trim().to_string())
.filter(|u| !u.is_empty())
{
let client = reqwest::Client::new();
let payload = serde_json::json!({
"type": "community_grant",
"id": id.clone(),
"org_name": org_name,
"ein": if ein.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(ein.to_string()) },
"contact_name": contact_name,
"contact_email": contact_email,
"mission": mission,
"technical_objective": technical_objective,
"submitted_at": submitted_at,
});
let grant_id = id.clone();
tokio::spawn(async move {
if let Err(e) = client
.post(url.trim())
.json(&payload)
.timeout(std::time::Duration::from_secs(10))
.send()
.await
{
tracing::warn!("community grant webhook failed: {}", e);
} else {
tracing::info!("community grant webhook sent for {}", grant_id);
}
});
}
let loc = format!(
"/community-grant/confirmed?ref={}",
urlencoding::encode(&id)
);
Redirect::temporary(loc.as_str()).into_response()
}
fn confirmed_html(ref_id: Option<&str>) -> String {
let ref_line = ref_id
.filter(|s| !s.is_empty())
.map(|r| {
format!(
r#"<p class="intake-detail">Reference ID: <code>{}</code></p>"#,
html_escape(r)
)
})
.unwrap_or_default();
let content = format!(
r#"<section class="intake-section intake-done"><div class="intake-doc intake-complete"><div class="intake-check-icon">✓</div><h1>Application Received</h1><p class="intake-success">Your Community Grant application has been submitted.</p>{}<p class="intake-detail">Cochranblock will review applications and contact selected applicants within 2–3 business days of the quarter close.</p><a href="/community-grant" class="btn">Done</a></div></section>"#,
ref_line
);
format!(
"{}{}{}{}",
f62(
"community-grant-confirmed",
"Application Received | CochranBlock"
),
C7,
content,
C8
)
}
/// GET /community-grant/confirmed
pub async fn confirmed(Query(q): Query<std::collections::HashMap<String, String>>) -> Html<String> {
let ref_id = q.get("ref").map(|s| s.as_str());
Html(confirmed_html(ref_id))
}