simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit fb434d9dc0869e369758fd5a54dd6127bca91c44
parent dd198be281d7cff5b02d8995ccaa9f0aa010f7be
Author: Silas Brack <silasbrack@gmail.com>
Date:   Mon,  8 Jun 2026 22:33:20 +0200

feat: swap comments via fetch instead of full page reload

On SSE event, fetch /story/{id}/comments HTML fragment and swap
the comments section innerHTML instead of reloading the page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Diffstat:
Msrc/handlers/comment.rs | 47++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/routes.rs | 1+
Msrc/templates.rs | 7+++++++
Mtemplates/story.html | 9++++++---
4 files changed, 60 insertions(+), 4 deletions(-)

diff --git a/src/handlers/comment.rs b/src/handlers/comment.rs @@ -7,10 +7,11 @@ use axum::{ use axum_extra::extract::cookie::CookieJar; use serde::Deserialize; +use crate::db::CommentWithMeta; use crate::error::AppError; use crate::handlers::auth::get_tokens_from_cookies; use crate::state::AppState; -use crate::templates::{ApplicationTemplate, ReplyFormTemplate}; +use crate::templates::{ApplicationTemplate, CommentsTemplate, ReplyFormTemplate}; use crate::trailbase; #[derive(Debug, Deserialize)] @@ -56,6 +57,50 @@ pub async fn create_comment( Ok(Redirect::to(&format!("/story/{}", story_id))) } +/// Get comments HTML fragment for realtime updates +pub async fn get_comments( + State(state): State<AppState>, + jar: CookieJar, + Path(story_id): Path<i64>, +) -> Result<Html<String>, AppError> { + let tokens = get_tokens_from_cookies(&jar); + let client = match &tokens { + Some(t) => trailbase::client_with_tokens(&state.trailbase_url, t)?, + None => trailbase::new_client(&state.trailbase_url)?, + }; + + let comments = trailbase::get_comments_for_story(&client, story_id).await?; + + let comment_ids: Vec<i64> = comments.iter().map(|c| c.id).collect(); + let voted_comment_ids = if tokens.is_some() && !comment_ids.is_empty() { + trailbase::get_user_votes(&client, trailbase::TARGET_TYPE_COMMENT, &comment_ids) + .await + .inspect_err(|e| tracing::error!("Failed to get comment votes: {}", e)) + .unwrap_or_default() + } else { + vec![] + }; + + let mut enriched = Vec::with_capacity(comments.len()); + for comment in comments { + let author = trailbase::get_user_profile_by_id(&client, &comment.created_by) + .await + .inspect_err(|e| tracing::error!("Failed to get profile for {}: {}", comment.created_by, e)) + .ok() + .flatten() + .map(|p| p.username); + let time_ago = trailbase::time_ago(&comment.created_at); + let voted = voted_comment_ids.contains(&comment.id); + enriched.push(CommentWithMeta::from_comment(comment, author, time_ago, voted)); + } + + let template = CommentsTemplate { + comments: enriched, + is_logged_in: tokens.is_some(), + }; + Ok(Html(template.render()?)) +} + /// Show reply form for a comment pub async fn show_reply_form( State(state): State<AppState>, diff --git a/src/routes.rs b/src/routes.rs @@ -70,6 +70,7 @@ pub fn create_router(state: AppState) -> Router { .route("/story/{id}/edit", post(handlers::edit_story)) // Comments + .route("/story/{id}/comments", get(handlers::get_comments)) .route("/story/{id}/comment", post(handlers::create_comment)) .route("/comment/{id}/reply", get(handlers::show_reply_form)) .route("/comment/{id}/reply", post(handlers::submit_reply)) diff --git a/src/templates.rs b/src/templates.rs @@ -31,6 +31,13 @@ pub struct StoryTemplate { } #[derive(Template)] +#[template(path = "comments.html")] +pub struct CommentsTemplate { + pub comments: Vec<CommentWithMeta>, + pub is_logged_in: bool, +} + +#[derive(Template)] #[template(path = "submit.html")] pub struct SubmitTemplate { pub error: Option<String>, diff --git a/templates/story.html b/templates/story.html @@ -87,9 +87,12 @@ <script> (function() { var evtSource = new EventSource('/api/records/v1/comment/subscribe/*?filter[story_id]={{ story.id }}'); - evtSource.onmessage = function(event) { - window.location.reload(); - }; + evtSource.onmessage = function() { + fetch('/story/{{ story.id }}/comments') + .then(function(r) { return r.text(); }) + .then(function(html) { + document.getElementById('comments-section').innerHTML = html; + }); })(); </script> </div>