diff --git a/backend/scss/styles.scss b/backend/scss/styles.scss index 5446a6a..9db3558 100644 --- a/backend/scss/styles.scss +++ b/backend/scss/styles.scss @@ -9,16 +9,6 @@ body { font-family: "JetBrains Mono", monospace; } -// * { -// font-family: "JetBrains Mono", monospace; -// } - -h1, -h2, -h3 { - text-align: center; -} - h2 { text-decoration: underline; } diff --git a/backend/src/cache.rs b/backend/src/cache.rs index 185983f..3527d77 100644 --- a/backend/src/cache.rs +++ b/backend/src/cache.rs @@ -34,6 +34,8 @@ pub mod keys { format!("{}{}", POST, id) } + pub const TAGS: &str = "tags"; + pub const TAG_POSTS: &str = "tag_posts:"; pub fn tag_posts(id: i32) -> String { @@ -115,7 +117,7 @@ pub fn cache_posts( } #[derive(Serialize, Deserialize, Debug)] -pub struct Post { +pub struct PostWithoutDescription { pub name: String, pub slug: String, pub published_at: NaiveDate, @@ -124,11 +126,21 @@ pub struct Post { pub content: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct PostWithoutContent { + pub name: String, + pub slug: String, + pub description: String, + pub published_at: NaiveDate, + pub edited_at: Option, + pub tags: Vec, +} + pub fn cache_post( id: i32, db_conn: &mut db::Connection, redis_conn: &mut Connection, -) -> Result { +) -> Result { let key = keys::post(id); if let Some(s) = redis::cmd("GET") @@ -141,7 +153,7 @@ pub fn cache_post( .filter(db::schema::posts::id.eq(id)) .first::(db_conn)?; - let post_cache = Post { + let post_cache = PostWithoutDescription { name: post.name, slug: post.slug, published_at: post.published_at, @@ -163,6 +175,98 @@ pub fn cache_post( } } +#[derive(Serialize, Deserialize, Debug)] +pub struct PostsByTag { + pub name: String, + pub posts: Vec, +} + +pub fn cache_tags( + db_conn: &mut db::Connection, + redis_conn: &mut Connection, +) -> Result> { + if let Some(s) = redis::cmd("GET") + .arg(keys::TAGS) + .query::>(redis_conn)? + { + Ok(serde_json::from_str(&s)?) + } else { + let tags = db::schema::tags::table + .select((db::schema::tags::id, db::schema::tags::name)) + .order(db::schema::tags::name) + .load::<(i32, String)>(db_conn)?; + + let posts_by_tag = tags + .into_iter() + .map(|(id, name)| -> Result<_> { + let post_tag_post_ids = db::schema::post_tags::table + .select(db::schema::post_tags::post_id) + .filter(db::schema::post_tags::tag_id.eq(id)) + .load::(db_conn)?; + + let posts = db::schema::posts::table + .select(( + db::schema::posts::id, + db::schema::posts::name, + db::schema::posts::slug, + db::schema::posts::description, + db::schema::posts::published_at, + db::schema::posts::edited_at, + )) + .filter(db::schema::posts::active) + .filter(db::schema::posts::id.eq_any(post_tag_post_ids)) + .order(db::schema::posts::published_at.desc()) + .load::<(i32, String, String, String, NaiveDate, Option)>(db_conn)?; + + Ok( + PostsByTag { + name, + posts: + posts + .into_iter() + .map( + |p: ( + i32, + String, + String, + String, + NaiveDate, + Option, + )| + -> Result<_> { + let (id, name, slug, description, published_at, edited_at) = + p; + let tags = web::get_tags_by_post(id, db_conn)?; + + Ok(PostWithoutContent { + name, + slug, + description, + published_at, + edited_at, + tags, + }) + }, + ) + .collect::>>()?, + }, + ) + }) + .collect::>>()?; + + redis::cmd("SET") + .arg(keys::TAGS) + .arg(serde_json::to_string(&posts_by_tag)?) + .query(redis_conn)?; + redis::cmd("EXPIRE") + .arg(keys::TAGS) + .arg(config::CONFIG.cache_ttl) + .query(redis_conn)?; + + Ok(posts_by_tag) + } +} + pub fn cache_tag_posts( id: i32, db_conn: &mut db::Connection, diff --git a/backend/src/web/mod.rs b/backend/src/web/mod.rs index 0cad2ee..cd84b28 100644 --- a/backend/src/web/mod.rs +++ b/backend/src/web/mod.rs @@ -8,19 +8,18 @@ use crate::{cache, db}; pub mod templates; +use templates::TemplateToResponseWithStatusCode; + pub mod static_dir { include!(concat!(env!("OUT_DIR"), "/generated.rs")); } async fn not_found() -> HttpResponse { - let mut resp = templates::StatusCode { + templates::StatusCode { status_code: http::StatusCode::NOT_FOUND, message: Some("maybe try a correct url?".to_string()), } - .to_response(); - *resp.status_mut() = http::StatusCode::NOT_FOUND; - - resp + .to_response_with_status_code(http::StatusCode::NOT_FOUND) } #[get("/")] @@ -89,7 +88,7 @@ async fn post_by_slug( if let Some(post_id) = match db::schema::posts::table .select(db::schema::posts::id) - .filter(db::schema::posts::slug.eq(&slug)) + .filter(db::schema::posts::slug.eq(slug)) .filter(db::schema::posts::active) .first::(db_conn) .optional() @@ -104,125 +103,34 @@ async fn post_by_slug( templates::PostBySlug { post }.to_response() } else { - let mut resp = templates::StatusCode { + templates::StatusCode { status_code: http::StatusCode::NOT_FOUND, message: Some("this post does not exists... yet".to_string()), } - .to_response(); - *resp.status_mut() = http::StatusCode::NOT_FOUND; - - return resp; + .to_response_with_status_code(http::StatusCode::NOT_FOUND) } +} - // let post_stripped: Option<(i32, String, NaiveDate, Option)> = - // match db::schema::posts::table - // .select(( - // db::schema::posts::id, - // db::schema::posts::name, - // db::schema::posts::published_at, - // db::schema::posts::edited_at, - // )) - // .filter(db::schema::posts::slug.eq(&slug)) - // .filter(db::schema::posts::active) - // .first::<(i32, String, NaiveDate, Option)>(db_conn) - // .optional() - // { - // Ok(x) => x, - // Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), - // }; - // - // match post_stripped { - // Some(stripped) => { - // let (stripped_id, stripped_name, stripped_published_at, stripped_edited_at) = stripped; +#[get("/tags")] +async fn tags( + db_pool: web::Data, + redis_pool: web::Data, +) -> HttpResponse { + let db_conn = &mut match db_pool.get() { + Ok(x) => x, + Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), + }; + let redis_conn = &mut match redis_pool.get() { + Ok(x) => x, + Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), + }; - // let key = cache::keys::post_content(stripped_id); + let x = match cache::cache_tags(db_conn, redis_conn) { + Ok(x) => x, + Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), + }; - // match match redis::cmd("GET") - // .arg(&key) - // .query::>(redis_conn.deref_mut()) - // { - // Ok(x) => x, - // Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)), - // } { - // Some(s) => { - // let tags = match get_tags_by_post(stripped_id, db_conn) { - // Ok(x) => x, - // Err(e) => { - // return HttpResponse::InternalServerError().body(format!("{:?}", e)) - // } - // }; - - // templates::PostBySlug { - // name: stripped_name, - // slug, - // published_at: stripped_published_at, - // edited_at: stripped_edited_at, - // tags, - // content: s, - // } - // } - // .to_response(), - // None => { - // let post = match db::schema::posts::table - // .filter(db::schema::posts::id.eq(stripped_id)) - // .first::(db_conn) - // { - // Ok(x) => x, - // Err(e) => { - // return HttpResponse::InternalServerError().body(format!("{:?}", e)) - // } - // }; - - // let html = markdown::to_html(&post.content); - - // match redis::cmd("SET") - // .arg(&key) - // .arg(&html) - // .query::>(redis_conn.deref_mut()) - // { - // Ok(x) => x, - // Err(e) => { - // return HttpResponse::InternalServerError().body(format!("{:?}", e)) - // } - // }; - // if let Err(e) = redis::cmd("EXPIRE") - // .arg(key) - // .arg(config::CONFIG.cache_ttl) - // .query::<()>(redis_conn.deref_mut()) - // { - // return HttpResponse::InternalServerError().body(format!("{:?}", e)); - // } - - // let tags = match get_tags_by_post(stripped_id, db_conn) { - // Ok(x) => x, - // Err(e) => { - // return HttpResponse::InternalServerError().body(format!("{:?}", e)) - // } - // }; - - // templates::PostBySlug { - // name: post.name, - // slug: post.slug, - // published_at: stripped_published_at, - // edited_at: stripped_edited_at, - // tags, - // content: html, - // } - // .to_response() - // } - // } - // } - // None => { - // let mut resp = templates::StatusCode { - // status_code: http::StatusCode::NOT_FOUND, - // message: Some("this post does not exists... yet".to_string()), - // } - // .to_response(); - // *resp.status_mut() = http::StatusCode::NOT_FOUND; - - // resp - // } - // } + templates::Tags { tags: x }.to_response() } #[get("/tags/{name}")] @@ -272,6 +180,7 @@ fn setup_routes(cfg: &mut web::ServiceConfig) { .service(posts) .service(ResourceFiles::new("/static", generated)) .service(post_by_slug) + .service(tags) .service(tag_by_name) .default_service(web::route().to(not_found)); } diff --git a/backend/src/web/templates.rs b/backend/src/web/templates.rs index 1b1e583..c5bb4c6 100644 --- a/backend/src/web/templates.rs +++ b/backend/src/web/templates.rs @@ -1,10 +1,44 @@ -use actix_web::http; +use actix_web::{body::BoxBody, http, HttpResponse, HttpResponseBuilder, ResponseError}; use askama_actix::Template; use chrono::prelude::*; use serde::{Deserialize, Serialize}; +use std::fmt; use crate::cache; +struct ActixError(askama::Error); + +impl fmt::Debug for ActixError { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(&self.0, f) + } +} + +impl fmt::Display for ActixError { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(&self.0, f) + } +} + +impl ResponseError for ActixError {} + +pub trait TemplateToResponseWithStatusCode { + fn to_response_with_status_code(&self, code: http::StatusCode) -> HttpResponse; +} + +impl TemplateToResponseWithStatusCode for T { + fn to_response_with_status_code(&self, code: http::StatusCode) -> HttpResponse { + match self.render() { + Ok(buffer) => HttpResponseBuilder::new(code) + .content_type(http::header::HeaderValue::from_static(T::MIME_TYPE)) + .body(buffer), + Err(err) => HttpResponse::from_error(ActixError(err)), + } + } +} + #[derive(Template)] #[template(path = "status_code.html")] pub struct StatusCode { @@ -39,13 +73,13 @@ pub struct Posts { #[derive(Template)] #[template(path = "web/posts/{slug}.html")] pub struct PostBySlug { - // pub name: String, - // pub slug: String, - // pub published_at: NaiveDate, - // pub edited_at: Option, - // pub tags: Vec, - // pub content: String, - pub post: cache::Post, + pub post: cache::PostWithoutDescription, +} + +#[derive(Template)] +#[template(path = "web/tags/index.html")] +pub struct Tags { + pub tags: Vec, } #[derive(Template)] diff --git a/backend/templates/web/posts/index.html b/backend/templates/web/posts/index.html index 4b22012..8a75ff8 100644 --- a/backend/templates/web/posts/index.html +++ b/backend/templates/web/posts/index.html @@ -9,6 +9,7 @@ {% endblock %} {% block content %} +

Post archive

    {% for post in posts %}
  • diff --git a/backend/templates/web/tags/index.html b/backend/templates/web/tags/index.html new file mode 100644 index 0000000..fc51563 --- /dev/null +++ b/backend/templates/web/tags/index.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %}posts{% endblock %} + +{% block head %}{% endblock %} + +{% block breadcrumb %} +
  • posts
  • +{% endblock %} + +{% block content %} +

    Tags

    +{% for tag in tags %} +
    +

    {{ tag.name }}

    +
      + {% for post in tag.posts %} +
    • + + {{ post.name }} + ({{ post.published_at }}{% match post.edited_at %}{% when Some with (x) %} -> {{ x }}{% when None %}{% endmatch %}) + +
      +
        + {% for tag in post.tags %} +
      • {{ tag }}
      • + {% endfor %} +
      +
      + {{ post.description }} +
    • + {% endfor %} +
    +
    +{% endfor %} +{% endblock %}