Compare commits
3 Commits
7affe88d50
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be69a54fd1 | ||
|
|
90aa4f8185 | ||
|
|
5d5f9ec765 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT p.post_id, p.author_id, u.username AS author,\n p.title, p.content, p.published_at, last_modified\n FROM posts p\n LEFT JOIN users u ON p.author_id = u.user_id\n WHERE p.post_id = $1\n ",
|
"query": "\n SELECT p.post_id, p.author_id, u.username AS author, u.full_name,\n p.title, p.content, p.published_at, last_modified\n FROM posts p\n LEFT JOIN users u ON p.author_id = u.user_id\n WHERE p.post_id = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -20,21 +20,26 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"name": "title",
|
"name": "full_name",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 4,
|
"ordinal": 4,
|
||||||
"name": "content",
|
"name": "title",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
|
"name": "content",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
"name": "published_at",
|
"name": "published_at",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 6,
|
"ordinal": 7,
|
||||||
"name": "last_modified",
|
"name": "last_modified",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
@@ -48,11 +53,12 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "ccffe61c27508d32cf43556a8bffa465f24fec8416a4884ead4eafd324feea72"
|
"hash": "059162eba48cf5f519d0d8b6ce63575ced91941b8c55c986b8c5591c7d9b09e4"
|
||||||
}
|
}
|
||||||
12
.sqlx/query-1e1a90042e89bd8662df3bae15bc7506146cff102034664c77ab0fc68b9480f5.json
generated
Normal file
12
.sqlx/query-1e1a90042e89bd8662df3bae15bc7506146cff102034664c77ab0fc68b9480f5.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n DELETE FROM idempotency\n WHERE created_at < NOW() - INTERVAL '1 hour'\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "1e1a90042e89bd8662df3bae15bc7506146cff102034664c77ab0fc68b9480f5"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT p.author_id, u.username as author,\n p.post_id, p.title, p.content, p.published_at, p.last_modified\n FROM posts p\n INNER JOIN users u ON p.author_id = u.user_id\n WHERE p.author_id = $1\n ORDER BY p.published_at DESC\n ",
|
"query": "\n SELECT p.author_id, u.username as author, u.full_name,\n p.post_id, p.title, p.content, p.published_at, p.last_modified\n FROM posts p\n INNER JOIN users u ON p.author_id = u.user_id\n WHERE p.author_id = $1\n ORDER BY p.published_at DESC\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -15,26 +15,31 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 2,
|
"ordinal": 2,
|
||||||
|
"name": "full_name",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
"name": "post_id",
|
"name": "post_id",
|
||||||
"type_info": "Uuid"
|
"type_info": "Uuid"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 4,
|
||||||
"name": "title",
|
"name": "title",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 4,
|
"ordinal": 5,
|
||||||
"name": "content",
|
"name": "content",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 5,
|
"ordinal": 6,
|
||||||
"name": "published_at",
|
"name": "published_at",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 6,
|
"ordinal": 7,
|
||||||
"name": "last_modified",
|
"name": "last_modified",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
@@ -47,6 +52,7 @@
|
|||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -54,5 +60,5 @@
|
|||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "c545267390019d45c5b4b32caf6c46928ffc7bdac46828cf7f1104ef67f42391"
|
"hash": "1fc92c14786c21d24951341e3a8149964533b7627d2d073eeac7b7d3230513ce"
|
||||||
}
|
}
|
||||||
12
.sqlx/query-7eccf0027753bc1c42897aef12c9350eca023f3be52e24530127d06c3c449104.json
generated
Normal file
12
.sqlx/query-7eccf0027753bc1c42897aef12c9350eca023f3be52e24530127d06c3c449104.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n DELETE FROM subscriptions\n WHERE status = 'pending_confirmation'\n AND subscribed_at < NOW() - INTERVAL '24 hours'\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "7eccf0027753bc1c42897aef12c9350eca023f3be52e24530127d06c3c449104"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT p.post_id, p.author_id, u.username AS author,\n p.title, p.content, p.published_at, p.last_modified\n FROM posts p\n LEFT JOIN users u ON p.author_id = u.user_id\n ORDER BY p.published_at DESC\n LIMIT $1\n OFFSET $2\n ",
|
"query": "\n SELECT p.post_id, p.author_id, u.username AS author, u.full_name,\n p.title, p.content, p.published_at, p.last_modified\n FROM posts p\n LEFT JOIN users u ON p.author_id = u.user_id\n ORDER BY p.published_at DESC\n LIMIT $1\n OFFSET $2\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -20,21 +20,26 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"name": "title",
|
"name": "full_name",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 4,
|
"ordinal": 4,
|
||||||
"name": "content",
|
"name": "title",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
|
"name": "content",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
"name": "published_at",
|
"name": "published_at",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 6,
|
"ordinal": 7,
|
||||||
"name": "last_modified",
|
"name": "last_modified",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
@@ -49,11 +54,12 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "836bd296bffff9a2ec14e43ea6aa64a468aaf0914bd95297431320621b42e396"
|
"hash": "dc3c1b786b4f4bd65f625922ce05eab4cb161f3de6c6e676af778f7749af5710"
|
||||||
}
|
}
|
||||||
58
src/database_worker.rs
Normal file
58
src/database_worker.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use sqlx::{
|
||||||
|
PgPool,
|
||||||
|
postgres::{PgConnectOptions, PgPoolOptions},
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub async fn run_until_stopped(configuration: PgConnectOptions) -> Result<(), anyhow::Error> {
|
||||||
|
let connection_pool = PgPoolOptions::new().connect_lazy_with(configuration);
|
||||||
|
worker_loop(connection_pool).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn worker_loop(connection_pool: PgPool) -> Result<(), anyhow::Error> {
|
||||||
|
loop {
|
||||||
|
if let Err(e) = clean_pending_subscriptions(&connection_pool).await {
|
||||||
|
tracing::error!("{:?}", e);
|
||||||
|
}
|
||||||
|
if let Err(e) = clean_idempotency_keys(&connection_pool).await {
|
||||||
|
tracing::error!("{:?}", e);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clean_pending_subscriptions(connection_pool: &PgPool) -> Result<(), anyhow::Error> {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM subscriptions
|
||||||
|
WHERE status = 'pending_confirmation'
|
||||||
|
AND subscribed_at < NOW() - INTERVAL '24 hours'
|
||||||
|
"
|
||||||
|
)
|
||||||
|
.execute(connection_pool)
|
||||||
|
.await
|
||||||
|
.context("Failed to clean up subscriptions table.")?;
|
||||||
|
match result.rows_affected() {
|
||||||
|
n if n > 0 => tracing::info!("Cleaned up {} expired subscriptions.", n),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clean_idempotency_keys(connection_pool: &PgPool) -> Result<(), anyhow::Error> {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM idempotency
|
||||||
|
WHERE created_at < NOW() - INTERVAL '1 hour'
|
||||||
|
"
|
||||||
|
)
|
||||||
|
.execute(connection_pool)
|
||||||
|
.await
|
||||||
|
.context("Failed to clean up idempontency table.")?;
|
||||||
|
match result.rows_affected() {
|
||||||
|
n if n > 0 => tracing::info!("Cleaned up {} old idempotency records.", n),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -13,6 +13,6 @@ pub struct CommentEntry {
|
|||||||
|
|
||||||
impl CommentEntry {
|
impl CommentEntry {
|
||||||
pub fn formatted_date(&self) -> String {
|
pub fn formatted_date(&self) -> String {
|
||||||
self.published_at.format("%B %d, %Y").to_string()
|
self.published_at.format("%B %d, %Y %H:%M").to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub struct PostEntry {
|
|||||||
pub post_id: Uuid,
|
pub post_id: Uuid,
|
||||||
pub author_id: Uuid,
|
pub author_id: Uuid,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
|
pub full_name: Option<String>,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub published_at: DateTime<Utc>,
|
pub published_at: DateTime<Utc>,
|
||||||
@@ -13,13 +14,9 @@ pub struct PostEntry {
|
|||||||
|
|
||||||
impl PostEntry {
|
impl PostEntry {
|
||||||
pub fn formatted_date(&self) -> String {
|
pub fn formatted_date(&self) -> String {
|
||||||
self.published_at.format("%B %d, %Y").to_string()
|
self.published_at.format("%B %d, %Y %H:%M").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn last_modified(&self) -> String {
|
|
||||||
// if let Some(last_modified) = self.last_modi
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn to_html(&self) -> anyhow::Result<String> {
|
pub fn to_html(&self) -> anyhow::Result<String> {
|
||||||
match markdown::to_html_with_options(&self.content, &markdown::Options::gfm()) {
|
match markdown::to_html_with_options(&self.content, &markdown::Options::gfm()) {
|
||||||
Ok(content) => Ok(content),
|
Ok(content) => Ok(content),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::time::Duration;
|
|||||||
use tracing::{Span, field::display};
|
use tracing::{Span, field::display};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub async fn run_worker_until_stopped(configuration: Settings) -> Result<(), anyhow::Error> {
|
pub async fn run_until_stopped(configuration: Settings) -> Result<(), anyhow::Error> {
|
||||||
let connection_pool = PgPoolOptions::new().connect_lazy_with(configuration.database.with_db());
|
let connection_pool = PgPoolOptions::new().connect_lazy_with(configuration.database.with_db());
|
||||||
let email_client = EmailClient::build(configuration.email_client).unwrap();
|
let email_client = EmailClient::build(configuration.email_client).unwrap();
|
||||||
worker_loop(connection_pool, email_client).await
|
worker_loop(connection_pool, email_client).await
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod authentication;
|
pub mod authentication;
|
||||||
pub mod configuration;
|
pub mod configuration;
|
||||||
|
pub mod database_worker;
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
pub mod email_client;
|
pub mod email_client;
|
||||||
pub mod idempotency;
|
pub mod idempotency;
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@@ -1,6 +1,6 @@
|
|||||||
use zero2prod::{
|
use zero2prod::{
|
||||||
configuration::get_configuration, issue_delivery_worker::run_worker_until_stopped,
|
configuration::get_configuration, database_worker, issue_delivery_worker, startup::Application,
|
||||||
startup::Application, telemetry::init_subscriber,
|
telemetry::init_subscriber,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -11,11 +11,16 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
let application = Application::build(configuration.clone()).await?;
|
let application = Application::build(configuration.clone()).await?;
|
||||||
|
|
||||||
let application_task = tokio::spawn(application.run_until_stopped());
|
let application_task = tokio::spawn(application.run_until_stopped());
|
||||||
let worker_task = tokio::spawn(run_worker_until_stopped(configuration));
|
let database_worker_task = tokio::spawn(database_worker::run_until_stopped(
|
||||||
|
configuration.database.with_db(),
|
||||||
|
));
|
||||||
|
let delivery_worker_task =
|
||||||
|
tokio::spawn(issue_delivery_worker::run_until_stopped(configuration));
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = application_task => {},
|
_ = application_task => {},
|
||||||
_ = worker_task => {},
|
_ = database_worker_task => {},
|
||||||
|
_ = delivery_worker_task => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ async fn get_posts(
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
PostEntry,
|
PostEntry,
|
||||||
r#"
|
r#"
|
||||||
SELECT p.post_id, p.author_id, u.username AS author,
|
SELECT p.post_id, p.author_id, u.username AS author, u.full_name,
|
||||||
p.title, p.content, p.published_at, p.last_modified
|
p.title, p.content, p.published_at, p.last_modified
|
||||||
FROM posts p
|
FROM posts p
|
||||||
LEFT JOIN users u ON p.author_id = u.user_id
|
LEFT JOIN users u ON p.author_id = u.user_id
|
||||||
@@ -101,7 +101,7 @@ pub async fn get_posts_page(
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
PostEntry,
|
PostEntry,
|
||||||
r#"
|
r#"
|
||||||
SELECT p.post_id, p.author_id, u.username AS author,
|
SELECT p.post_id, p.author_id, u.username AS author, u.full_name,
|
||||||
p.title, p.content, p.published_at, p.last_modified
|
p.title, p.content, p.published_at, p.last_modified
|
||||||
FROM posts p
|
FROM posts p
|
||||||
LEFT JOIN users u ON p.author_id = u.user_id
|
LEFT JOIN users u ON p.author_id = u.user_id
|
||||||
@@ -261,7 +261,7 @@ async fn get_post_data(
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
PostEntry,
|
PostEntry,
|
||||||
r#"
|
r#"
|
||||||
SELECT p.post_id, p.author_id, u.username AS author,
|
SELECT p.post_id, p.author_id, u.username AS author, u.full_name,
|
||||||
p.title, p.content, p.published_at, last_modified
|
p.title, p.content, p.published_at, last_modified
|
||||||
FROM posts p
|
FROM posts p
|
||||||
LEFT JOIN users u ON p.author_id = u.user_id
|
LEFT JOIN users u ON p.author_id = u.user_id
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ async fn fetch_user_posts(
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
PostEntry,
|
PostEntry,
|
||||||
r#"
|
r#"
|
||||||
SELECT p.author_id, u.username as author,
|
SELECT p.author_id, u.username as author, u.full_name,
|
||||||
p.post_id, p.title, p.content, p.published_at, p.last_modified
|
p.post_id, p.title, p.content, p.published_at, p.last_modified
|
||||||
FROM posts p
|
FROM posts p
|
||||||
INNER JOIN users u ON p.author_id = u.user_id
|
INNER JOIN users u ON p.author_id = u.user_id
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ where
|
|||||||
)
|
)
|
||||||
.with(
|
.with(
|
||||||
tracing_subscriber::fmt::layer()
|
tracing_subscriber::fmt::layer()
|
||||||
.compact()
|
.pretty()
|
||||||
.with_writer(sink)
|
.with_writer(sink)
|
||||||
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE),
|
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
{% if let Some(user_id) = comment.user_id %}
|
{% if let Some(user_id) = comment.user_id %}
|
||||||
<a href="/users/{{ comment.username.as_ref().unwrap() }}"
|
<a href="/users/{{ comment.username.as_ref().unwrap() }}"
|
||||||
class="font-semibold text-blue-600 hover:text-blue-800 hover:underline">
|
class="font-semibold text-blue-800 hover:text-blue-600 hover:underline">
|
||||||
{{ comment.username.as_ref().unwrap() }}
|
{{ comment.username.as_ref().unwrap() }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<a href="/posts/{{ post.post_id }}">
|
<a href="/posts/{{ post.post_id }}">
|
||||||
<h2 class="text-xl font-semibold text-gray-900 mb-3 hover:text-blue-600 transition-colors">{{
|
<h2 class="text-xl font-semibold text-gray-900 mb-3 hover:text-blue-600 transition-colors">
|
||||||
post.title }}</h2>
|
{{
|
||||||
|
post.title }}
|
||||||
|
</h2>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex items-center text-sm text-gray-500 mb-1">
|
<div class="flex items-center text-sm text-gray-500 mb-1">
|
||||||
<svg class="w-4 h-4 mr-1"
|
<svg class="w-4 h-4 mr-1"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor">
|
stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<time datetime="{{ post.published_at }}">
|
<time datetime="{{ post.published_at }}">
|
||||||
{{ post.formatted_date() }}
|
{{ post.formatted_date() }}
|
||||||
@@ -23,11 +24,16 @@
|
|||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor">
|
stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<a href="/users/{{ post.author }}"
|
<a href="/users/{{ post.author }}"
|
||||||
class="hover:text-blue-600 hover:underline">{{ post.author }}</a>
|
class="hover:text-blue-600 hover:underline">
|
||||||
|
{% if let Some(full_name) = post.full_name %}
|
||||||
|
{{ full_name }}
|
||||||
|
{% else %}
|
||||||
|
{{ post.author }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="/posts/{{ post.post_id }}" class="flex-shrink-0 ml-4">
|
<a href="/posts/{{ post.post_id }}" class="flex-shrink-0 ml-4">
|
||||||
@@ -35,7 +41,7 @@
|
|||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor">
|
stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,13 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<a href="/users/{{ post.author }}"
|
<a href="/users/{{ post.author }}"
|
||||||
class="hover:text-blue-600 hover:underline font-medium">{{ post.author }}</a>
|
class="hover:text-blue-600 hover:underline font-medium">
|
||||||
|
{% if let Some(full_name) = post.full_name %}
|
||||||
|
{{ full_name }}
|
||||||
|
{% else %}
|
||||||
|
{{ post.author }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<svg class="w-4 h-4 mr-1 text-gray-400"
|
<svg class="w-4 h-4 mr-1 text-gray-400"
|
||||||
@@ -47,7 +53,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if let Some(modified) = post.last_modified %}
|
{% if let Some(modified) = post.last_modified %}
|
||||||
<span class="text-sm italic text-gray-500">Last modified on {{ modified.format("%B %d, %Y") }}, at {{ modified.format("%H:%M") }}</span>
|
<span class="text-sm italic text-gray-500">Last modified on {{ modified.format("%B %d, %Y") }} at {{ modified.format("%H:%M") }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
{% if session_user_id.as_ref() == Some(post.author_id) %}
|
{% if session_user_id.as_ref() == Some(post.author_id) %}
|
||||||
|
|||||||
Reference in New Issue
Block a user