add: finish forge stuff

This commit is contained in:
trisua 2025-06-10 22:02:06 -04:00
parent 53fb4d5778
commit 68071b96c8
21 changed files with 329 additions and 18 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "tetratto"
version = "5.0.0"
version = "6.0.0"
edition = "2024"
[features]

View file

@ -119,6 +119,7 @@ pub const STACKS_MANAGE: &str = include_str!("./public/html/stacks/manage.lisp")
pub const FORGE_HOME: &str = include_str!("./public/html/forge/home.lisp");
pub const FORGE_BASE: &str = include_str!("./public/html/forge/base.lisp");
pub const FORGE_INFO: &str = include_str!("./public/html/forge/info.lisp");
pub const FORGE_TICKETS: &str = include_str!("./public/html/forge/tickets.lisp");
// langs
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
@ -392,6 +393,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
write_template!(html_path->"forge/home.html"(crate::assets::FORGE_HOME) -d "forge" --config=config --lisp plugins);
write_template!(html_path->"forge/base.html"(crate::assets::FORGE_BASE) --config=config --lisp plugins);
write_template!(html_path->"forge/info.html"(crate::assets::FORGE_INFO) --config=config --lisp plugins);
write_template!(html_path->"forge/tickets.html"(crate::assets::FORGE_TICKETS) --config=config --lisp plugins);
html_path
}

View file

@ -208,3 +208,5 @@ version = "1.0.0"
"forge:label.create_new" = "Create new forge"
"forge:tab.info" = "Info"
"forge:tab.tickets" = "Tickets"
"forge:action.reopen" = "Reopen"
"forge:action.close" = "Close"

View file

@ -27,6 +27,7 @@
--color-red: hsl(0, 84%, 40%);
--color-green: hsl(100, 84%, 20%);
--color-yellow: hsl(41, 63%, 75%);
--color-purple: hsl(284, 84%, 20%);
--radius: 6px;
--circle: 360px;
--shadow-x-offset: 0;
@ -63,6 +64,7 @@
--color-red: hsl(0, 94%, 82%);
--color-green: hsl(100, 94%, 82%);
--color-yellow: hsl(41, 63%, 65%);
--color-purple: hsl(284, 94%, 82%);
}
* {

View file

@ -179,6 +179,14 @@
color: var(--color-green) !important;
}
.yellow {
color: var(--color-yellow) !important;
}
.purple {
color: var(--color-purple) !important;
}
.hidden {
display: none !important;
}

View file

@ -115,7 +115,6 @@
(div
("style" "display: contents")
(text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}"))
(text "{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false, repost=false, expect_repost=false, poll=false, dont_show_title=false) -%} {% if community and show_community and community.id != config.town_square or question %}")
(div
("class" "card-nest")
@ -174,7 +173,19 @@
("class" "flex items-center")
("style" "color: var(--color-primary)")
(text "{{ icon \"square-asterisk\" }}"))
(text "{%- endif %} {% if post.context.repost and post.context.repost.reposting %}")
(text "{%- endif %}")
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
(span
("title" "Open")
("class" "flex items-center green")
(text "{{ icon \"circle-dot\" }}"))
(text "{% else %}")
(span
("title" "Closed")
("class" "flex items-center purple")
(text "{{ icon \"circle-check\" }}"))
(text "{%- endif %} {%- endif %}")
(text "{% if post.context.repost and post.context.repost.reposting %}")
(span
("title" "Repost")
("class" "flex items-center")
@ -215,7 +226,7 @@
; title
(text "{% if post.title and community and community.context.enable_titles -%}")
(h2 (text "{{ post.title }}"))
(hr ("class" "margin"))
(hr ("class" "margin") ("style" "margin-top: var(--pad-2)"))
(text "{%- endif %}")
; content
@ -323,7 +334,23 @@
(text "{{ icon \"quote\" }}")
(span
(text "{{ text \"communities:label.quote_post\" }}")))
(text "{%- endif %} {% if user.id != post.owner -%}")
(text "{%- endif %}")
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
(button
("class" "green")
("onclick" "trigger('me::update_open', ['{{ post.id }}', false])")
(text "{{ icon \"circle-check\" }}")
(span
(text "{{ text \"forge:action.close\" }}")))
(text "{% else %}")
(button
("class" "purple")
("onclick" "trigger('me::update_open', ['{{ post.id }}', true])")
(text "{{ icon \"refresh-ccw-dot\" }}")
(span
(text "{{ text \"forge:action.reopen\" }}")))
(text "{%- endif %} {%- endif %}")
(text "{% if user.id != post.owner -%}")
(b
("class" "title")
(text "{{ text \"general:label.safety\" }}"))
@ -1746,3 +1773,42 @@
(text "{%- endif %}"))
(text "{%- endif %}")
(text "{%- endmacro %}")
(text "{% macro ticket(post, owner) -%}")
(div
("href" "/post/{{ post.id }}")
("class" "card secondary w-fill flex flex-col gap-2")
(div
("class" "flex gap-2 items-center")
; user info
(a
("href" "/@{{ owner.username }}")
(text "{{ self::avatar(username=owner.username, size=\"24px\", selector_type=\"username\") }}"))
(span
("class" "name")
(text "{{ self::full_username(user=owner) }}"))
; timestamp
(span ("class" "date") (text "{{ post.created }}")))
; post title
(a
("href" "/post/{{ post.id }}")
("class" "flush flex gap-2 items-center")
; open/closed icon
(text "{% if community and community.is_forge -%} {% if post.is_open -%}")
(span
("title" "Open")
("class" "flex items-center green")
(text "{{ icon \"circle-dot\" }}"))
(text "{% else %}")
(span
("title" "Closed")
("class" "flex items-center purple")
(text "{{ icon \"circle-check\" }}"))
(text "{%- endif %} {%- endif %}")
(h4
("class" "no_p_margin")
(text "{{ post.title|markdown|safe }}"))))
(text "{%- endmacro %}")

View file

@ -3,4 +3,5 @@
("class" "flex flex-col gap-4 w-full")
(text "{{ macros::forge_nav(community=community, selected=\"info\") }}")
(text "{{ components::community_info(community=community) }}"))
; (text "{{ components::community_banner(id=community.id, community=community) }}")
(text "{% endblock %}")

View file

@ -0,0 +1,18 @@
(text "{% extends \"forge/base.html\" %} {% block content %}")
(div
("class" "flex flex-col gap-4 w-full")
(text "{{ macros::forge_nav(community=community, selected=\"tickets\") }}")
(div
("class" "card-nest")
(div
("class" "card small flex items-center gap-2")
(icon (text "circle-dot"))
(str (text "forge:tab.tickets")))
(div
("class" "card flex flex-col gap-2")
(text "{% for post in feed -%}")
(text "{{ components::ticket(post=post[0], owner=post[1]) }}")
(text "{%- endfor %}")
(text "{{ components::pagination(page=page, items=feed|length) }}"))))
(text "{% endblock %}")

View file

@ -135,6 +135,31 @@
});
});
self.define("update_open", async (_, id, status) => {
if (
!(await trigger("atto::confirm", [
"Are you sure you want to do this?",
]))
) {
return;
}
fetch(`/api/v1/posts/${id}/open`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ open: status }),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
});
self.define("react", async (_, element, asset, asset_type, is_like) => {
await trigger("atto::debounce", ["reactions::toggle"]);
fetch("/api/v1/reactions", {

View file

@ -15,7 +15,10 @@ use tetratto_core::model::{
use crate::{
get_user_from_token,
image::{save_webp_buffer, JsonMultipart},
routes::api::v1::{CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext, VoteInPoll},
routes::api::v1::{
CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext, UpdatePostIsOpen,
VoteInPoll,
},
State,
};
@ -383,3 +386,25 @@ pub async fn vote_request(
Err(e) => Json(e.into()),
}
}
pub async fn update_is_open_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Json(req): Json<UpdatePostIsOpen>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.update_post_is_open(id, user, req.open).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Post updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -121,6 +121,10 @@ pub fn routes() -> Router {
"/posts/{id}/poll_vote",
post(communities::posts::vote_request),
)
.route(
"/posts/{id}/open",
post(communities::posts::update_is_open_request),
)
// drafts
.route("/drafts", post(communities::drafts::create_request))
.route("/drafts/{id}", delete(communities::drafts::delete_request))
@ -639,3 +643,8 @@ pub struct VoteInPoll {
pub struct AppendAssociations {
pub tokens: Vec<String>,
}
#[derive(Deserialize)]
pub struct UpdatePostIsOpen {
pub open: bool,
}

View file

@ -1,11 +1,11 @@
use super::{communities::community_context, render_error};
use super::{communities::community_context, render_error, PaginatedQuery};
use crate::{
assets::initial_context, check_community_permissions, community_context_bools, get_lang,
get_user_from_token, State,
};
use axum::{
extract::{Path, Query},
response::{Html, IntoResponse},
extract::Path,
Extension,
};
use axum_extra::extract::CookieJar;
@ -124,3 +124,93 @@ pub async fn info_request(
// return
Ok(Html(data.1.render("forge/info.html", &context).unwrap()))
}
/// `/forge/{title}/tickets`
pub async fn tickets_request(
jar: CookieJar,
Path(title): Path<String>,
Query(props): Query<PaginatedQuery>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = data.read().await;
let user = get_user_from_token!(jar, data.0);
let community = match data.0.get_community_by_title(&title.to_lowercase()).await {
Ok(ua) => ua,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
if community.id == 0 {
// don't show page for void community
return Err(Html(
render_error(
Error::GeneralNotFound("community".to_string()),
&jar,
&data,
&user,
)
.await,
));
}
// check permissions
let (can_read, _) = check_community_permissions!(community, jar, data, user);
// ...
let ignore_users = crate::ignore_users_gen!(user, data);
let feed = match data
.0
.get_posts_by_community(community.id, 12, props.page)
.await
{
Ok(p) => match data.0.fill_posts(p, &ignore_users, &user).await {
Ok(p) => p,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
// let pinned = match data.0.get_pinned_posts_by_community(community.id).await {
// Ok(p) => match data.0.fill_posts(p, &ignore_users, &user).await {
// Ok(p) => p,
// Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
// },
// Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
// };
// init context
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await;
let (
is_owner,
is_joined,
is_pending,
can_post,
can_manage_posts,
can_manage_community,
can_manage_roles,
can_manage_questions,
) = community_context_bools!(data, user, community);
context.insert("feed", &feed);
// context.insert("pinned", &pinned);
context.insert("page", &props.page);
community_context(
&mut context,
&community,
is_owner,
is_joined,
is_pending,
can_post,
can_read,
can_manage_posts,
can_manage_community,
can_manage_roles,
can_manage_questions,
);
// return
Ok(Html(data.1.render("forge/tickets.html", &context).unwrap()))
}

View file

@ -114,6 +114,7 @@ pub fn routes() -> Router {
// forge
.route("/forges", get(forge::home_request))
.route("/forge/{title}", get(forge::info_request))
.route("/forge/{title}/tickets", get(forge::tickets_request))
.route("/forge/{title}/members", get(communities::members_request))
// stacks
.route("/stacks", get(stacks::list_request))