use crate::routes::get_max_page; use crate::templates::PostsPageDashboardTemplate; use crate::{ domain::PostEntry, routes::{ AppError, Path, Query, fetch_comments_count, fetch_comments_page, get_comments_page_count, not_found_html, }, startup::AppState, templates::{HtmlTemplate, PostListTemplate, PostTemplate, PostsTemplate}, }; use anyhow::Context; use askama::Template; use axum::{ extract::State, response::{Html, IntoResponse, Redirect, Response}, }; use sqlx::PgPool; use uuid::Uuid; pub const POSTS_PER_PAGE: i64 = 3; #[tracing::instrument(name = "Fetching most recent posts from database", skip_all)] pub async fn list_posts( State(AppState { connection_pool, .. }): State, ) -> Result { let count = get_posts_count(&connection_pool) .await .context("Could not fetch posts table size.") .map_err(AppError::unexpected_page)?; let next_page = if count > POSTS_PER_PAGE { Some(2) } else { None }; let posts = get_posts(&connection_pool, POSTS_PER_PAGE, None) .await .context("Could not fetch latest posts") .map_err(AppError::unexpected_page)?; let template = PostsTemplate { posts, next_page }; Ok(Html(template.render().unwrap()).into_response()) } #[tracing::instrument(name = "Fetching next posts from database", skip_all)] pub async fn get_posts_page_dashboard( State(AppState { connection_pool, .. }): State, Query(LoadMoreParams { page }): Query, ) -> Result { let posts = get_posts_page(&connection_pool, page) .await .context("Could not fetch next posts page.")?; let posts_current_page = page; let count = get_posts_count(&connection_pool) .await .context("Could not fetch number of posts.")?; let posts_max_page = get_max_page(count, POSTS_PER_PAGE); let template = HtmlTemplate(PostsPageDashboardTemplate { posts, posts_current_page, posts_max_page, }); Ok(template.into_response()) } async fn get_posts( connection_pool: &PgPool, n: i64, offset: Option, ) -> Result, sqlx::Error> { sqlx::query_as!( PostEntry, r#" SELECT p.post_id, u.username AS author, p.title, p.content, p.published_at FROM posts p LEFT JOIN users u ON p.author_id = u.user_id ORDER BY p.published_at DESC LIMIT $1 OFFSET $2 "#, n, offset ) .fetch_all(connection_pool) .await } pub async fn get_posts_page( connection_pool: &PgPool, page: i64, ) -> Result, sqlx::Error> { let offset = (page - 1) * POSTS_PER_PAGE; sqlx::query_as!( PostEntry, r#" SELECT p.post_id, u.username AS author, p.title, p.content, p.published_at FROM posts p LEFT JOIN users u ON p.author_id = u.user_id ORDER BY p.published_at DESC LIMIT $1 OFFSET $2 "#, POSTS_PER_PAGE, offset ) .fetch_all(connection_pool) .await } pub async fn get_posts_count(connection_pool: &PgPool) -> Result { sqlx::query!("SELECT count(*) FROM posts") .fetch_one(connection_pool) .await .map(|r| r.count.unwrap()) } #[derive(serde::Deserialize)] pub struct PostParams { origin: Option, } #[tracing::instrument(name = "Fetching post from database", skip(connection_pool, origin))] pub async fn see_post( State(AppState { connection_pool, .. }): State, Path(post_id): Path, Query(PostParams { origin }): Query, ) -> Result { if let Some(origin) = origin { mark_email_as_opened(&connection_pool, origin).await?; return Ok(Redirect::to(&format!("/posts/{}", post_id)).into_response()); } if let Some(post) = get_post_data(&connection_pool, post_id) .await .context(format!("Failed to fetch post #{}", post_id)) .map_err(AppError::unexpected_page)? { let post = post .to_html() .context("Could not render markdown with extension.")?; let current_page = 1; let comments_count = fetch_comments_count(&connection_pool, post_id) .await .context("Could not fetch comment count")?; let max_page = get_comments_page_count(comments_count); let comments = fetch_comments_page(&connection_pool, post_id, 1) .await .context("Failed to fetch latest comments")?; let template = HtmlTemplate(PostTemplate { post, comments, current_page, max_page, comments_count, }); Ok(template.into_response()) } else { Ok(not_found_html()) } } #[tracing::instrument(name = "Mark email notification as opened", skip(connection_pool))] async fn mark_email_as_opened(connection_pool: &PgPool, email_id: Uuid) -> Result<(), AppError> { sqlx::query!( "UPDATE notifications_delivered SET opened = TRUE WHERE email_id = $1", email_id, ) .execute(connection_pool) .await .context("Failed to mark email as opened.") .map_err(AppError::unexpected_page)?; Ok(()) } async fn get_post_data( connection_pool: &PgPool, post_id: Uuid, ) -> Result, sqlx::Error> { sqlx::query_as!( PostEntry, r#" SELECT p.post_id, u.username AS author, p.title, p.content, p.published_at FROM posts p LEFT JOIN users u ON p.author_id = u.user_id WHERE p.post_id = $1 "#, post_id ) .fetch_optional(connection_pool) .await } #[derive(serde::Deserialize)] pub struct LoadMoreParams { page: i64, } #[tracing::instrument(name = "Fetching next posts in the database", skip(connection_pool))] pub async fn load_more( State(AppState { connection_pool, .. }): State, Query(LoadMoreParams { page }): Query, ) -> Result { let posts = get_posts_page(&connection_pool, page) .await .context("Could not fetch posts from database.")?; let count = get_posts_count(&connection_pool) .await .context("Could not fetch posts count.")?; let max_page = get_max_page(count, POSTS_PER_PAGE); Ok(Html( PostListTemplate { posts, next_page: if page < max_page { Some(page + 1) } else { None }, } .render() .unwrap(), ) .into_response()) }