Skip to main content

max / makenotwork

1.9 KB · 66 lines History Blame Raw
1 //! Search handler — full-text fuzzy search returning HTMX fragments.
2
3 use axum::{
4 extract::Query,
5 http::StatusCode,
6 response::{IntoResponse, Response},
7 };
8 use serde::Deserialize;
9
10 use crate::templates::*;
11 use crate::AppState;
12
13 #[derive(Deserialize)]
14 pub(super) struct SearchQuery {
15 pub(super) q: Option<String>,
16 pub(super) scope: Option<String>,
17 }
18
19 /// GET /search?q=...&scope=... — returns HTML fragment for HTMX swap.
20 #[tracing::instrument(skip_all)]
21 pub(super) async fn search_handler(
22 axum::extract::State(state): axum::extract::State<AppState>,
23 Query(query): Query<SearchQuery>,
24 ) -> Result<impl IntoResponse, Response> {
25 let q = query.q.as_deref().unwrap_or("").trim();
26
27 if q.is_empty() || q.len() < 2 {
28 return Ok(SearchResultsFragment { results: vec![] });
29 }
30
31 // Sanitize: limit length (find a char boundary to avoid UTF-8 panic)
32 let q = if q.len() > 200 {
33 let mut end = 200;
34 while !q.is_char_boundary(end) { end -= 1; }
35 &q[..end]
36 } else {
37 q
38 };
39
40 let scope = query.scope.as_deref().filter(|s| !s.is_empty());
41
42 let db_results = mt_db::queries::search_threads(&state.db, q, scope, 20)
43 .await
44 .map_err(|e| {
45 tracing::error!(error = ?e, "db error searching");
46 StatusCode::INTERNAL_SERVER_ERROR.into_response()
47 })?;
48
49 let results = db_results
50 .into_iter()
51 .map(|r| SearchResultViewRow {
52 thread_id: r.thread_id.to_string(),
53 thread_title: r.thread_title,
54 author_username: r.author_username,
55 community_name: r.community_name,
56 community_slug: r.community_slug,
57 category_name: r.category_name,
58 category_slug: r.category_slug,
59 snippet: r.snippet,
60 last_activity: mt_core::time_format::relative_timestamp(r.last_activity_at),
61 })
62 .collect();
63
64 Ok(SearchResultsFragment { results })
65 }
66