comment.rs (3328B)
1 use askama::Template; 2 use axum::{ 3 extract::{Path, State}, 4 response::{Html, Redirect}, 5 Form, 6 }; 7 use axum_extra::extract::cookie::CookieJar; 8 use serde::Deserialize; 9 10 use crate::error::AppError; 11 use crate::state::AppState; 12 use crate::templates::{ApplicationTemplate, ReplyFormTemplate}; 13 14 #[derive(Debug, Deserialize)] 15 pub struct CommentForm { 16 pub text: String, 17 } 18 19 /// Create a top-level comment on a story 20 pub async fn create_comment( 21 State(state): State<AppState>, 22 jar: CookieJar, 23 Path(story_id): Path<i64>, 24 Form(form): Form<CommentForm>, 25 ) -> Result<Redirect, Html<String>> { 26 let user_id = state.get_user_id(&jar).await.ok_or_else(|| { 27 Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) 28 })?; 29 30 if form.text.trim().is_empty() { 31 return Err(Html("Comment text is required".to_string())); 32 } 33 34 let text = form.text.trim().to_string(); 35 let comment_tx = state.comment_tx.clone(); 36 37 state.db().create_comment(user_id, story_id, None, &text) 38 .await.map_err(|e| Html(format!("Failed to create comment: {}", e)))?; 39 40 // Notify SSE subscribers 41 let _ = comment_tx.send(story_id); 42 43 Ok(Redirect::to(&format!("/story/{}", story_id))) 44 } 45 46 /// Show reply form for a comment 47 pub async fn show_reply_form( 48 State(state): State<AppState>, 49 jar: CookieJar, 50 Path(comment_id): Path<i64>, 51 ) -> Result<Html<String>, AppError> { 52 let user_id = state.get_user_id(&jar).await; 53 54 if user_id.is_none() { 55 return Ok(Html( 56 r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string(), 57 )); 58 } 59 60 let db = state.db(); 61 let parent_comment = db.get_comment(comment_id).await? 62 .ok_or_else(|| AppError::NotFound("Comment not found".to_string()))?; 63 64 let parent_author = if let Ok(uid) = parent_comment.created_by.parse::<i64>() { 65 db.get_user_by_id(uid) 66 .await 67 .ok() 68 .flatten() 69 .map(|u| u.username) 70 } else { 71 None 72 }; 73 74 let reply_form = ReplyFormTemplate { 75 comment_id, 76 story_id: parent_comment.story_id, 77 parent_text: parent_comment.text, 78 parent_author, 79 error: None, 80 }; 81 82 let app = ApplicationTemplate { 83 content: reply_form.render()?, 84 }; 85 Ok(Html(app.render()?)) 86 } 87 88 #[derive(Debug, Deserialize)] 89 pub struct ReplyForm { 90 pub text: String, 91 pub story_id: i64, 92 } 93 94 /// Submit a reply to a comment 95 pub async fn submit_reply( 96 State(state): State<AppState>, 97 jar: CookieJar, 98 Path(parent_id): Path<i64>, 99 Form(form): Form<ReplyForm>, 100 ) -> Result<Redirect, Html<String>> { 101 let user_id = state.get_user_id(&jar).await.ok_or_else(|| { 102 Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) 103 })?; 104 105 if form.text.trim().is_empty() { 106 return Err(Html("Reply text is required".to_string())); 107 } 108 109 let text = form.text.trim().to_string(); 110 let story_id = form.story_id; 111 let comment_tx = state.comment_tx.clone(); 112 113 state.db().create_comment(user_id, story_id, Some(parent_id), &text) 114 .await.map_err(|e| Html(format!("Failed to create reply: {}", e)))?; 115 116 // Notify SSE subscribers 117 let _ = comment_tx.send(story_id); 118 119 Ok(Redirect::to(&format!("/story/{}", story_id))) 120 }