//! Project embed handlers. use axum::{ extract::{Path, State}, response::{Html, IntoResponse, Response}, }; use crate::{ db::{self}, error::{AppError, Result}, AppState, }; use super::item::set_embed_headers; #[tracing::instrument(skip_all, name = "embed::project_card")] /// GET /embed/p/{project_slug}/card pub(super) async fn project_card( State(state): State, Path(project_slug): Path, ) -> Result { let project = db::projects::get_public_project_by_slug_str(&state.db, &project_slug) .await? .ok_or(AppError::NotFound)?; let user = db::users::get_user_by_id(&state.db, project.user_id) .await? .ok_or(AppError::NotFound)?; if user.is_suspended() || user.is_deactivated() { return Err(AppError::NotFound); } let items = db::items::get_public_items_by_project(&state.db, project.id).await?; let item_count = items.len(); let description_excerpt: String = project.description.as_deref() .unwrap_or("") .chars() .take(150) .collect(); let project_url = format!("{}/p/{}", state.config.host_url, project_slug); let profile_url = format!("{}/u/{}", state.config.host_url, user.username); let creator_name = user.display_name.as_deref().unwrap_or(&user.username); let category_label = project.project_type.label(); let cover_html = project.cover_image_url.as_ref() .map(|url| format!(r#""#, html_escape(url))) .unwrap_or_default(); let html = format!( r#" {title} — Makenot.work
{cover}
{title}
{item_count} items · {category}
{desc_html} View
"#, title = html_escape(&project.title), cover = cover_html, profile_url = profile_url, creator = html_escape(creator_name), item_count = item_count, category = category_label, desc_html = if description_excerpt.is_empty() { String::new() } else { format!(r#"
{}
"#, html_escape(&description_excerpt)) }, project_url = project_url, ); let mut response = Html(html).into_response(); set_embed_headers(&mut response); Ok(response) } fn html_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) }