Parent and child posts

This commit is contained in:
Bad Manners 2025-04-08 23:10:10 -03:00
parent 04f888c323
commit 54379b98e0
16 changed files with 334 additions and 58 deletions

View file

@ -4,8 +4,11 @@ Sam's small image board. Currently a WIP.
## TODO ## TODO
- [ ] Parent posts (including tags and stuff)
- [ ] Edit tags in the middle of input
- [ ] Pools - [ ] Pools
- [ ] Video support - [ ] Video support
- [ ] Cleanup/fixup background tasks
- [ ] CSS - [ ] CSS
- [ ] CLI, env vars, logging... - [ ] CLI, env vars, logging...

View file

@ -32,6 +32,29 @@ impl MigrationTrait for Migration {
) )
.await?; .await?;
manager
.create_table(
Table::create()
.table(SameyTag::Table)
.if_not_exists()
.col(pk_auto(SameyTag::Id))
.col(string_len(SameyTag::Name, 100))
.col(string_len_uniq(SameyTag::NormalizedName, 100))
.to_owned(),
)
.await?;
manager
.create_table(
Table::create()
.table(SameyPool::Table)
.if_not_exists()
.col(pk_auto(SameyPool::Id))
.col(string_len_uniq(SameyPool::Name, 100))
.to_owned(),
)
.await?;
manager manager
.create_table( .create_table(
Table::create() Table::create()
@ -98,18 +121,6 @@ impl MigrationTrait for Migration {
) )
.await?; .await?;
manager
.create_table(
Table::create()
.table(SameyTag::Table)
.if_not_exists()
.col(pk_auto(SameyTag::Id))
.col(string_len(SameyTag::Name, 100))
.col(string_len_uniq(SameyTag::NormalizedName, 100))
.to_owned(),
)
.await?;
manager manager
.create_table( .create_table(
Table::create() Table::create()
@ -142,6 +153,39 @@ impl MigrationTrait for Migration {
) )
.await?; .await?;
manager
.create_table(
Table::create()
.table(SameyPoolPost::Table)
.if_not_exists()
.col(pk_auto(SameyPoolPost::Id))
.col(integer(SameyPoolPost::PoolId))
.col(integer(SameyPoolPost::PostId))
.col(float(SameyPoolPost::Position))
.foreign_key(
ForeignKeyCreateStatement::new()
.name("fk-samey_pool_post-samey_pool-pool_id")
.from(SameyPoolPost::Table, SameyPoolPost::PoolId)
.to(SameyPool::Table, SameyPool::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKeyCreateStatement::new()
.name("fk-samey_pool_post-samey_post-post_id")
.from(SameyPoolPost::Table, SameyPoolPost::PostId)
.to(SameyPost::Table, SameyPost::Id)
.on_delete(ForeignKeyAction::Cascade),
)
.index(
Index::create()
.unique()
.col(SameyPoolPost::PoolId)
.col(SameyPoolPost::PostId),
)
.to_owned(),
)
.await?;
Ok(()) Ok(())
} }
@ -149,11 +193,11 @@ impl MigrationTrait for Migration {
// Replace the sample below with your own migration scripts // Replace the sample below with your own migration scripts
manager manager
.drop_table(Table::drop().table(SameyTagPost::Table).to_owned()) .drop_table(Table::drop().table(SameyPoolPost::Table).to_owned())
.await?; .await?;
manager manager
.drop_table(Table::drop().table(SameyTag::Table).to_owned()) .drop_table(Table::drop().table(SameyTagPost::Table).to_owned())
.await?; .await?;
manager manager
@ -164,6 +208,14 @@ impl MigrationTrait for Migration {
.drop_table(Table::drop().table(SameyPost::Table).to_owned()) .drop_table(Table::drop().table(SameyPost::Table).to_owned())
.await?; .await?;
manager
.drop_table(Table::drop().table(SameyPool::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(SameyTag::Table).to_owned())
.await?;
manager manager
.drop_table(Table::drop().table(SameyUser::Table).to_owned()) .drop_table(Table::drop().table(SameyUser::Table).to_owned())
.await?; .await?;
@ -255,3 +307,21 @@ enum SameyTagPost {
TagId, TagId,
PostId, PostId,
} }
#[derive(DeriveIden)]
enum SameyPool {
#[sea_orm(iden = "samey_pool")]
Table,
Id,
Name,
}
#[derive(DeriveIden)]
enum SameyPoolPost {
#[sea_orm(iden = "samey_pool_post")]
Table,
Id,
PoolId,
PostId,
Position,
}

View file

@ -123,7 +123,7 @@ impl SessionStore for SessionStorage {
.map(|(k, v)| (k.clone(), v.clone())) .map(|(k, v)| (k.clone(), v.clone()))
.collect(), .collect(),
)), )),
expiry_date: Set(record.expiry_date.unix_timestamp().to_string()), expiry_date: Set(record.expiry_date.unix_timestamp()),
..Default::default() ..Default::default()
}) })
.exec(&self.db) .exec(&self.db)
@ -141,7 +141,7 @@ impl SessionStore for SessionStorage {
.map(|(k, v)| (k.clone(), v.clone())) .map(|(k, v)| (k.clone(), v.clone()))
.collect(), .collect(),
)), )),
expiry_date: Set(record.expiry_date.unix_timestamp().to_string()), expiry_date: Set(record.expiry_date.unix_timestamp()),
..Default::default() ..Default::default()
}) })
.filter(samey_session::Column::SessionId.eq(record.id.to_string())) .filter(samey_session::Column::SessionId.eq(record.id.to_string()))
@ -175,20 +175,9 @@ impl SessionStore for SessionStorage {
.iter() .iter()
.map(|(k, v)| (k.clone(), v.clone())) .map(|(k, v)| (k.clone(), v.clone()))
.collect(), .collect(),
expiry_date: match session.expiry_date.parse() { expiry_date: OffsetDateTime::from_unix_timestamp(session.expiry_date).map_err(
Ok(timestamp) => { |_| session_store::Error::Backend("Invalid timestamp for expiry date".into()),
OffsetDateTime::from_unix_timestamp(timestamp).map_err(|_| { )?,
session_store::Error::Backend(
"Invalid timestamp for expiry date".into(),
)
})?
}
Err(_) => {
return Err(session_store::Error::Backend(
"Failed to parse session expiry date".into(),
));
}
},
}, },
None => return Ok(None), None => return Ok(None),
}; };

View file

@ -2,6 +2,8 @@
pub mod prelude; pub mod prelude;
pub mod samey_pool;
pub mod samey_pool_post;
pub mod samey_post; pub mod samey_post;
pub mod samey_post_source; pub mod samey_post_source;
pub mod samey_session; pub mod samey_session;

View file

@ -1,5 +1,7 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8 //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
pub use super::samey_pool::Entity as SameyPool;
pub use super::samey_pool_post::Entity as SameyPoolPost;
pub use super::samey_post::Entity as SameyPost; pub use super::samey_post::Entity as SameyPost;
pub use super::samey_post_source::Entity as SameyPostSource; pub use super::samey_post_source::Entity as SameyPostSource;
pub use super::samey_session::Entity as SameySession; pub use super::samey_session::Entity as SameySession;

View file

@ -0,0 +1,26 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "samey_pool")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::samey_pool_post::Entity")]
SameyPoolPost,
}
impl Related<super::samey_pool_post::Entity> for Entity {
fn to() -> RelationDef {
Relation::SameyPoolPost.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -0,0 +1,48 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.8
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "samey_pool_post")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub pool_id: i32,
pub post_id: i32,
#[sea_orm(column_type = "Float")]
pub position: f32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::samey_pool::Entity",
from = "Column::PoolId",
to = "super::samey_pool::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
SameyPool,
#[sea_orm(
belongs_to = "super::samey_post::Entity",
from = "Column::PostId",
to = "super::samey_post::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
SameyPost,
}
impl Related<super::samey_pool::Entity> for Entity {
fn to() -> RelationDef {
Relation::SameyPool.def()
}
}
impl Related<super::samey_post::Entity> for Entity {
fn to() -> RelationDef {
Relation::SameyPost.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -24,6 +24,8 @@ pub struct Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm(has_many = "super::samey_pool_post::Entity")]
SameyPoolPost,
#[sea_orm( #[sea_orm(
belongs_to = "Entity", belongs_to = "Entity",
from = "Column::ParentId", from = "Column::ParentId",
@ -46,6 +48,12 @@ pub enum Relation {
SameyUser, SameyUser,
} }
impl Related<super::samey_pool_post::Entity> for Entity {
fn to() -> RelationDef {
Relation::SameyPoolPost.def()
}
}
impl Related<super::samey_post_source::Entity> for Entity { impl Related<super::samey_post_source::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::SameyPostSource.def() Relation::SameyPostSource.def()

View file

@ -7,9 +7,10 @@ use sea_orm::entity::prelude::*;
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i32, pub id: i32,
#[sea_orm(unique)]
pub session_id: String, pub session_id: String,
pub data: Json, pub data: Json,
pub expiry_date: String, pub expiry_date: i64,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -16,16 +16,17 @@ use crate::{
}; };
#[derive(Debug, FromQueryResult)] #[derive(Debug, FromQueryResult)]
pub(crate) struct SearchPost { pub(crate) struct PostOverview {
pub(crate) id: i32, pub(crate) id: i32,
pub(crate) thumbnail: String, pub(crate) thumbnail: String,
pub(crate) tags: String, pub(crate) tags: String,
pub(crate) rating: String,
} }
pub(crate) fn search_posts( pub(crate) fn search_posts(
tags: Option<&Vec<&str>>, tags: Option<&Vec<&str>>,
user: Option<User>, user: Option<&User>,
) -> Selector<SelectModel<SearchPost>> { ) -> Selector<SelectModel<PostOverview>> {
let mut include_tags = HashSet::<String>::new(); let mut include_tags = HashSet::<String>::new();
let mut exclude_tags = HashSet::<String>::new(); let mut exclude_tags = HashSet::<String>::new();
let mut include_ratings = HashSet::<String>::new(); let mut include_ratings = HashSet::<String>::new();
@ -47,11 +48,12 @@ pub(crate) fn search_posts(
} }
} }
let mut query = if include_tags.is_empty() && exclude_tags.is_empty() { let query = if include_tags.is_empty() && exclude_tags.is_empty() {
let mut query = SameyPost::find() let mut query = SameyPost::find()
.select_only() .select_only()
.column(samey_post::Column::Id) .column(samey_post::Column::Id)
.column(samey_post::Column::Thumbnail) .column(samey_post::Column::Thumbnail)
.column(samey_post::Column::Rating)
.column_as( .column_as(
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"), Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
"tags", "tags",
@ -73,6 +75,7 @@ pub(crate) fn search_posts(
.select_only() .select_only()
.column(samey_post::Column::Id) .column(samey_post::Column::Id)
.column(samey_post::Column::Thumbnail) .column(samey_post::Column::Thumbnail)
.column(samey_post::Column::Rating)
.column_as( .column_as(
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"), Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
"tags", "tags",
@ -130,20 +133,10 @@ pub(crate) fn search_posts(
query query
}; };
query = match user { filter_by_user(query, user)
None => query.filter(samey_post::Column::IsPublic.into_simple_expr()),
Some(user) if !user.is_admin => query.filter(
Condition::any()
.add(samey_post::Column::IsPublic.into_simple_expr())
.add(samey_post::Column::UploaderId.eq(user.id)),
),
_ => query,
};
query
.group_by(samey_post::Column::Id) .group_by(samey_post::Column::Id)
.order_by_desc(samey_post::Column::Id) .order_by_desc(samey_post::Column::Id)
.into_model::<SearchPost>() .into_model::<PostOverview>()
} }
pub(crate) fn get_tags_for_post(post_id: i32) -> Select<SameyTag> { pub(crate) fn get_tags_for_post(post_id: i32) -> Select<SameyTag> {
@ -152,3 +145,15 @@ pub(crate) fn get_tags_for_post(post_id: i32) -> Select<SameyTag> {
.filter(samey_tag_post::Column::PostId.eq(post_id)) .filter(samey_tag_post::Column::PostId.eq(post_id))
.order_by_asc(samey_tag::Column::Name) .order_by_asc(samey_tag::Column::Name)
} }
pub(crate) fn filter_by_user(query: Select<SameyPost>, user: Option<&User>) -> Select<SameyPost> {
match user {
None => query.filter(samey_post::Column::IsPublic.into_simple_expr()),
Some(user) if !user.is_admin => query.filter(
Condition::any()
.add(samey_post::Column::IsPublic.into_simple_expr())
.add(samey_post::Column::UploaderId.eq(user.id)),
),
_ => query,
}
}

View file

@ -30,7 +30,7 @@ use crate::{
samey_post, samey_post_source, samey_tag, samey_tag_post, samey_post, samey_post_source, samey_tag, samey_tag_post,
}, },
error::SameyError, error::SameyError,
query::{SearchPost, get_tags_for_post, search_posts}, query::{PostOverview, filter_by_user, get_tags_for_post, search_posts},
}; };
const MAX_THUMBNAIL_DIMENSION: u32 = 192; const MAX_THUMBNAIL_DIMENSION: u32 = 192;
@ -350,7 +350,7 @@ pub(crate) async fn select_tag(
struct PostsTemplate<'a> { struct PostsTemplate<'a> {
tags: Option<Vec<&'a str>>, tags: Option<Vec<&'a str>>,
tags_text: Option<String>, tags_text: Option<String>,
posts: Vec<SearchPost>, posts: Vec<PostOverview>,
page: u32, page: u32,
page_count: u64, page_count: u64,
} }
@ -378,7 +378,7 @@ pub(crate) async fn posts_page(
.tags .tags
.as_ref() .as_ref()
.map(|tags| tags.split_whitespace().collect::<Vec<_>>()); .map(|tags| tags.split_whitespace().collect::<Vec<_>>());
let pagination = search_posts(tags.as_ref(), auth_session.user).paginate(&db, 50); let pagination = search_posts(tags.as_ref(), auth_session.user.as_ref()).paginate(&db, 50);
let page_count = pagination.num_pages().await?; let page_count = pagination.num_pages().await?;
let posts = pagination.fetch_page(page.saturating_sub(1) as u64).await?; let posts = pagination.fetch_page(page.saturating_sub(1) as u64).await?;
let posts = posts let posts = posts
@ -386,7 +386,7 @@ pub(crate) async fn posts_page(
.map(|post| { .map(|post| {
let mut tags_vec: Vec<_> = post.tags.split_ascii_whitespace().collect(); let mut tags_vec: Vec<_> = post.tags.split_ascii_whitespace().collect();
tags_vec.sort(); tags_vec.sort();
SearchPost { PostOverview {
tags: tags_vec.into_iter().join(" "), tags: tags_vec.into_iter().join(" "),
..post ..post
} }
@ -412,8 +412,11 @@ pub(crate) async fn posts_page(
struct ViewPostTemplate { struct ViewPostTemplate {
post: samey_post::Model, post: samey_post::Model,
tags: Vec<samey_tag::Model>, tags: Vec<samey_tag::Model>,
tags_text: String,
sources: Vec<samey_post_source::Model>, sources: Vec<samey_post_source::Model>,
can_edit: bool, can_edit: bool,
parent_post: Option<PostOverview>,
children_posts: Vec<PostOverview>,
} }
pub(crate) async fn view_post( pub(crate) async fn view_post(
@ -421,18 +424,64 @@ pub(crate) async fn view_post(
auth_session: AuthSession, auth_session: AuthSession,
Path(post_id): Path<u32>, Path(post_id): Path<u32>,
) -> Result<impl IntoResponse, SameyError> { ) -> Result<impl IntoResponse, SameyError> {
let tags = get_tags_for_post(post_id as i32).all(&db).await?; let post_id = post_id as i32;
let tags = get_tags_for_post(post_id).all(&db).await?;
let tags_text = tags.iter().map(|tag| &tag.name).join(" ");
let sources = SameyPostSource::find() let sources = SameyPostSource::find()
.filter(samey_post_source::Column::PostId.eq(post_id)) .filter(samey_post_source::Column::PostId.eq(post_id))
.all(&db) .all(&db)
.await?; .await?;
let post = SameyPost::find_by_id(post_id as i32) let post = SameyPost::find_by_id(post_id)
.one(&db) .one(&db)
.await? .await?
.ok_or(SameyError::NotFound)?; .ok_or(SameyError::NotFound)?;
let parent_post = if let Some(parent_id) = post.parent_id {
match filter_by_user(SameyPost::find_by_id(parent_id), auth_session.user.as_ref())
.one(&db)
.await?
{
Some(parent_post) => Some(PostOverview {
id: parent_id,
thumbnail: parent_post.thumbnail,
tags: get_tags_for_post(post_id)
.all(&db)
.await?
.iter()
.map(|tag| &tag.name)
.join(" "),
rating: parent_post.rating,
}),
None => None,
}
} else {
None
};
let children_posts_models = filter_by_user(
SameyPost::find().filter(samey_post::Column::ParentId.eq(post_id)),
auth_session.user.as_ref(),
)
.all(&db)
.await?;
let mut children_posts = Vec::with_capacity(children_posts_models.capacity());
for child_post in children_posts_models.into_iter() {
children_posts.push(PostOverview {
id: child_post.id,
thumbnail: child_post.thumbnail,
tags: get_tags_for_post(child_post.id)
.all(&db)
.await?
.iter()
.map(|tag| &tag.name)
.join(" "),
rating: child_post.rating,
});
}
let can_edit = match auth_session.user { let can_edit = match auth_session.user {
None => false, None => false,
Some(user) => user.is_admin || post.uploader_id == user.id, Some(user) => user.is_admin || post.uploader_id == user.id,
@ -446,8 +495,11 @@ pub(crate) async fn view_post(
ViewPostTemplate { ViewPostTemplate {
post, post,
tags, tags,
tags_text,
sources, sources,
can_edit, can_edit,
parent_post,
children_posts,
} }
.render()?, .render()?,
)) ))
@ -505,12 +557,14 @@ pub(crate) struct SubmitPostDetailsForm {
#[serde(rename = "source")] #[serde(rename = "source")]
sources: Option<Vec<String>>, sources: Option<Vec<String>>,
tags: String, tags: String,
parent_post: String,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "submit_post_details.html")] #[template(path = "submit_post_details.html")]
struct SubmitPostDetailsTemplate { struct SubmitPostDetailsTemplate {
post: samey_post::Model, post: samey_post::Model,
parent_post: Option<PostOverview>,
sources: Vec<samey_post_source::Model>, sources: Vec<samey_post_source::Model>,
tags: Vec<samey_tag::Model>, tags: Vec<samey_tag::Model>,
can_edit: bool, can_edit: bool,
@ -529,7 +583,7 @@ pub(crate) async fn submit_post_details(
.await? .await?
.ok_or(SameyError::NotFound)?; .ok_or(SameyError::NotFound)?;
match auth_session.user { match auth_session.user.as_ref() {
None => return Err(SameyError::Forbidden), None => return Err(SameyError::Forbidden),
Some(user) => { Some(user) => {
if !user.is_admin && post.uploader_id != user.id { if !user.is_admin && post.uploader_id != user.id {
@ -546,6 +600,27 @@ pub(crate) async fn submit_post_details(
description if description.is_empty() => None, description if description.is_empty() => None,
description => Some(description.to_owned()), description => Some(description.to_owned()),
}; };
let parent_post = if let Some(parent_id) = body.parent_post.trim().parse().ok() {
match filter_by_user(SameyPost::find_by_id(parent_id), auth_session.user.as_ref())
.one(&db)
.await?
{
Some(parent_post) => Some(PostOverview {
id: parent_id,
thumbnail: parent_post.thumbnail,
tags: get_tags_for_post(post_id)
.all(&db)
.await?
.iter()
.map(|tag| &tag.name)
.join(" "),
rating: parent_post.rating,
}),
None => None,
}
} else {
None
};
let is_public = body.is_public.is_some(); let is_public = body.is_public.is_some();
let post = SameyPost::update(samey_post::ActiveModel { let post = SameyPost::update(samey_post::ActiveModel {
id: Set(post_id), id: Set(post_id),
@ -553,6 +628,7 @@ pub(crate) async fn submit_post_details(
description: Set(description), description: Set(description),
is_public: Set(is_public), is_public: Set(is_public),
rating: Set(body.rating), rating: Set(body.rating),
parent_id: Set(parent_post.as_ref().map(|post| post.id)),
..Default::default() ..Default::default()
}) })
.exec(&db) .exec(&db)
@ -622,6 +698,7 @@ pub(crate) async fn submit_post_details(
post, post,
sources, sources,
tags: upload_tags, tags: upload_tags,
parent_post,
can_edit: true, can_edit: true,
} }
.render()?, .render()?,

View file

@ -46,6 +46,10 @@
</ul> </ul>
<button hx-post="/post_source" hx-target="#sources" hx-swap="beforeend">+</button> <button hx-post="/post_source" hx-target="#sources" hx-swap="beforeend">+</button>
</div> </div>
<div>
<label>Parent post</label>
<input name="parent_post" type="text" value="{% if let Some(parent_id) = post.parent_id %}{{ parent_id }}{% endif %}" />
</div>
<div> <div>
<button>Submit</button> <button>Submit</button>
<button hx-get="/post_details/{{ post.id }}">Cancel</button> <button hx-get="/post_details/{{ post.id }}">Cancel</button>

View file

@ -20,7 +20,6 @@
hx-trigger="input changed delay:400ms" hx-trigger="input changed delay:400ms"
hx-target="next .tags-autocomplete" hx-target="next .tags-autocomplete"
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);" hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
value=""
autofocus autofocus
/> />
<ul class="tags-autocomplete" id="search-autocomplete"></ul> <ul class="tags-autocomplete" id="search-autocomplete"></ul>
@ -40,7 +39,6 @@
hx-trigger="input changed delay:400ms" hx-trigger="input changed delay:400ms"
hx-target="next .tags-autocomplete" hx-target="next .tags-autocomplete"
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);" hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
value=""
/> />
<ul class="tags-autocomplete" id="upload-autocomplete"></ul> <ul class="tags-autocomplete" id="upload-autocomplete"></ul>
<input <input
@ -52,6 +50,13 @@
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
</article> </article>
<article>
<h2>Create pool</h2>
<form method="post" action="/pool">
<input class="tags" type="text" id="pool-name" name="name" />
<button type="submit">Submit</button>
</form>
</article>
<article> <article>
<a href="/logout">Log out</a> <a href="/logout">Log out</a>
</article> </article>

View file

@ -46,6 +46,7 @@
title="{{ post.tags }}" title="{{ post.tags }}"
> >
<img src="/files/{{ post.thumbnail }}" /> <img src="/files/{{ post.thumbnail }}" />
<div>{{ post.rating }}</div>
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View file

@ -1,4 +1,14 @@
{% include "post_details.html" %} {% include "post_details.html" %} {% if let Some(parent_post) = parent_post %}
<article id="parent-post" hx-swap-oob="outerHTML">
<h2>Parent</h2>
<a href="/view/{{ parent_post.id }}" title="{{ parent_post.tags }}">
<img src="/files/{{ parent_post.thumbnail }}" />
<div>{{ parent_post.rating }}</div>
</a>
</article>
{% else %}
<article id="parent-post" hx-swap-oob="outerHTML" hidden></article>
{% endif %}
<ul id="tags-list" hx-swap-oob="outerHTML"> <ul id="tags-list" hx-swap-oob="outerHTML">
{% for tag in tags %} {% for tag in tags %}
<li> <li>

View file

@ -13,7 +13,7 @@
<meta property="og:image" content="/files/{{ post.media }}" /> <meta property="og:image" content="/files/{{ post.media }}" />
<meta property="og:image:width" content="{{ post.width }}" /> <meta property="og:image:width" content="{{ post.width }}" />
<meta property="og:image:height" content="{{ post.height }}" /> <meta property="og:image:height" content="{{ post.height }}" />
<!-- <meta property="og:image:alt" content="TO-DO" /> --> <meta property="og:image:alt" content="{{ tags_text }}" />
<meta <meta
property="og:description" property="og:description"
content="{% if let Some(description) = post.description %}{{ description }}{% endif %}" content="{% if let Some(description) = post.description %}{{ description }}{% endif %}"
@ -34,6 +34,31 @@
<h2>Details</h2> <h2>Details</h2>
{% include "post_details.html" %} {% include "post_details.html" %}
</article> </article>
{% if let Some(parent_post) = parent_post %}
<article id="parent-post">
<h2>Parent</h2>
<a href="/view/{{ parent_post.id }}" title="{{ parent_post.tags }}">
<img src="/files/{{ parent_post.thumbnail }}" />
<div>{{ parent_post.rating }}</div>
</a>
</article>
{% else %}
<article id="parent-post" hidden></article>
{% endif %} {% if !children_posts.is_empty() %}
<article>
<h2>Child posts</h2>
<ul>
{% for child_post in children_posts %}
<li>
<a href="/view/{{ child_post.id }}" title="{{ child_post.tags }}">
<img src="/files/{{ child_post.thumbnail }}" />
<div>{{ child_post.rating }}</div>
</a>
</li>
{% endfor %}
</ul>
</article>
{% endif %}
<article> <article>
<h2>Tags</h2> <h2>Tags</h2>
<ul id="tags-list"> <ul id="tags-list">