add: forges ui

TODO: forges tickets feed, posts open/closed state
This commit is contained in:
trisua 2025-06-09 16:45:36 -04:00
parent 5b1db42c51
commit a6140f7c8c
40 changed files with 1664 additions and 1270 deletions

View file

@ -0,0 +1,305 @@
(div ("id" "toast_zone"))
; random js
(text "<script data-turbo-permanent=\"true\" id=\"init-script\">
document.documentElement.addEventListener(\"turbo:load\", () => {
const atto = ns(\"atto\");
if (!atto) {
window.location.reload();
return;
}
atto.disconnect_observers();
atto.remove_false_options();
atto.clean_date_codes();
atto.clean_poll_date_codes();
atto.link_filter();
atto[\"hooks::scroll\"](document.body, document.documentElement);
atto[\"hooks::dropdown.init\"](window);
atto[\"hooks::character_counter.init\"]();
atto[\"hooks::long_text.init\"]();
atto[\"hooks::alt\"]();
atto[\"hooks::online_indicator\"]();
atto[\"hooks::ips\"]();
atto[\"hooks::check_reactions\"]();
atto[\"hooks::tabs\"]();
atto[\"hooks::spotify_time_text\"](); // spotify durations
atto[\"hooks::verify_emoji\"]();
if (document.getElementById(\"tokens\")) {
trigger(\"me::render_token_picker\", [
document.getElementById(\"tokens\"),
]);
}
setTimeout(() => {
trigger(\"me::notifications_stream\");
}, 250);
});
</script>")
(text "{% if user -%}
<script data-turbo-permanent=\"true\" id=\"update-seen-script\">
document.documentElement.addEventListener(\"turbo:load\", () => {
trigger(\"me::seen\");
trigger(\"streams::user\", [\"{{ user.id }}\"]);
if (!window.location.pathname.startsWith(\"/chats/\")) {
if (window.socket) {
window.socket.send(\"Close\");
window.socket = undefined;
console.log(\"socket disconnect\");
}
}
});
</script>
{%- endif %}")
; dialogs
(dialog
("id" "link_filter")
(div
("class" "inner flex flex-col gap-2")
; warning stuff
(p (text "Pressing continue will bring you to the following URL:"))
(pre (code ("id" "link_filter_url")))
(p (text "Are sure you want to go there?"))
(hr ("class" "margin"))
(div
("class" "flex gap-2")
(a
("class" "button primary")
("id" "link_filter_continue")
("rel" "noopener noreferrer")
("target" "_blank")
("onclick", "document.getElementById('link_filter').close()")
(icon (text "external-link"))
(str (text "dialog:action.continue")))
(button
("class" "secondary")
("type" "button")
("onclick", "document.getElementById('link_filter').close()")
(icon (text "x"))
(str (text "dialog:action.cancel"))))))
(dialog
("id" "web_api_prompt")
(div
("class" "inner flex flex-col gap-2")
(form
("class" "flex gap-2 flex-col")
("onsubmit" "event.preventDefault()")
(label ("for" "prompt") ("id" "web_api_prompt:msg"))
(input ("id" "prompt") ("name" "prompt"))
(div
("class" "flex justify-between")
(div null?)
(div
("class" "flex gap-2")
(button
("class" "primary bold circle")
("onclick", "globalThis.web_api_prompt_submit(document.getElementById('prompt').value); document.getElementById('prompt').value = ''")
("type" "button")
(icon (text "check"))
(str (text "dialog:action.okay")))
(button
("class" "bold red camo")
("onclick", "globalThis.web_api_prompt_submit('')")
("type" "button")
(icon (text "x"))
(str (text "dialog:action.cancel"))))))))
(dialog
("id" "web_api_prompt_long")
(div
("class" "inner flex flex-col gap-2")
(form
("class" "flex gap-2 flex-col")
("onsubmit" "event.preventDefault()")
(label ("for" "prompt_long") ("id" "web_api_prompt_long:msg"))
(input ("id" "prompt_long") ("name" "prompt_long"))
(div
("class" "flex justify-between")
(div null?)
(div
("class" "flex gap-2")
(button
("class" "primary bold circle")
("onclick", "globalThis.web_api_prompt_long_submit(document.getElementById('prompt_long').value); document.getElementById('prompt_long').value = ''")
("type" "button")
(icon (text "check"))
(str (text "dialog:action.okay")))
(button
("class" "bold red camo")
("onclick", "globalThis.web_api_prompt_long_submit('')")
("type" "button")
(icon (text "x"))
(str (text "dialog:action.cancel"))))))))
(dialog
("id" "web_api_confirm")
(div
("class" "inner flex flex-col gap-2")
(form
("class" "flex gap-2 flex-col")
("onsubmit" "event.preventDefault()")
(span ("id" "web_api_confirm:msg"))
(div
("class" "flex justify-between")
(div null?)
(div
("class" "flex gap-2")
(button
("class" "primary bold circle")
("onclick", "globalThis.web_api_confirm_submit(true)")
("type" "button")
(icon (text "check"))
(str (text "dialog:action.yes")))
(button
("class" "bold red camo")
("onclick", "globalThis.web_api_confirm_submit(false)")
("type" "button")
(icon (text "x"))
(str (text "dialog:action.no"))))))))
(div
("class" "lightbox hidden")
("id" "lightbox")
(button
("class" "lightbox_exit small square quaternary red")
("onclick" "trigger('ui::lightbox_close')")
(icon (text "x")))
(a
("href" "")
("id" "lightbox_img_a")
("target" "_blank")
(img ("id" "lightbox_img") ("loading" "lazy"))))
; tokens dialog
(text "{% if user -%}")
(dialog
("id" "tokens_dialog")
(div
("class" "inner flex flex-col gap-2")
(form
("class" "flex gap-2 flex-col")
("onsubmit" "event.preventDefault()")
(div ("id" "tokens") ("style" "display: contents"))
(div
("class" "flex justify-between")
(a
("href" "/auth/login")
("class" "button")
("data-turbo", "false")
(icon (text "plus"))
(span (str (text "general:action.add_account"))))
(button
("class" "quaternary")
("onclick" "document.getElementById('tokens_dialog').close()")
("type" "button")
(icon (text "check")))))))
; user scripts
(text "{%- endif %} {% if user and use_user_theme -%} {{ components::theme(user=user, theme_preference=user.settings.theme_preference) }}
<script>
setTimeout(() => {
trigger(\"atto::use_theme_preference\");
}, 150);
</script>
{%- endif %} {% if user and user.connections.Spotify and config.connections.spotify_client_id and user.connections.Spotify[0].data.token and user.connections.Spotify[0].data.refresh_token %}
<script>
setTimeout(async () => {
if (window.spotify_init) {
return;
}
window.spotify_init = true;
const client_id = \"{{ config.connections.spotify_client_id }}\";
let token = \"{{ user.connections.Spotify[0].data.token }}\";
let refresh_token =
\"{{ user.connections.Spotify[0].data.refresh_token }}\";
if (token) {
// we already have a token
const pull_playing = async () => {
const playing = await trigger(\"spotify::get_playing\", [
token,
]);
if (playing.error) {
// refresh token
const [new_token, new_refresh_token, expires_in] =
await trigger(\"spotify::refresh_token\", [
client_id,
refresh_token,
]);
await trigger(\"connections::push_con_data\", [
\"Spotify\",
{
token: new_token,
expires_in: expires_in.toString(),
name: profile.display_name,
},
]);
token = new_token;
refresh_token = new_refresh_token;
return;
}
await trigger(\"spotify::publish_playing\", [playing]);
};
await pull_playing();
setInterval(pull_playing, 30_000);
} else {
window.spotify_needs_token = true;
}
}, 150);
</script>
{% elif user and user.connections.LastFm and config.connections.last_fm_key and user.connections.LastFm[0].data.session_token %}
<script>
setTimeout(async () => {
if (window.last_fm_init) {
return;
}
window.last_fm_init = true;
const user = \"{{ user.connections.LastFm[0].data.name }}\";
const session_token =
\"{{ user.connections.LastFm[0].data.session_token }}\";
if (session_token) {
// we already have a token
const pull_playing = async () => {
const playing = await trigger(\"last_fm::get_playing\", [
user,
session_token,
]);
await trigger(\"last_fm::publish_playing\", [playing]);
};
await pull_playing();
setInterval(pull_playing, 30_000);
} else {
window.last_fm_needs_token = true;
}
}, 150);
</script>
{%- endif %}")

View file

@ -89,7 +89,7 @@
(div
("class" "w-full flex flex-col gap-2")
("id" "stream")
("style" "padding: 1rem")
("style" "padding: var(--pad-4)")
(turbo-frame
("id" "stream_body_frame")
("src" "/chats/{{ selected_community }}/{{ selected_channel }}/_stream?page={{ page }}&message={{ message }}"))
@ -222,7 +222,7 @@
}
.chats_nav button svg {
margin-right: 1rem;
margin-right: var(--pad-4);
}
.sidebar {
@ -238,7 +238,7 @@
}
.sidebar .title:not(.dropdown *) {
padding: 1rem;
padding: var(--pad-4);
border-bottom: solid 1px var(--color-super-lowered);
}
@ -272,7 +272,7 @@
}
.message.grouped {
padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 42px);
padding: var(--pad-1) var(--pad-4) var(--pad-1) calc(var(--pad-4) + var(--pad-2) + 42px);
}
turbo-frame {
@ -284,7 +284,7 @@
}
.members_list_half {
padding-top: 1rem;
padding-top: var(--pad-4);
border-top: solid 1px var(--color-super-lowered);
}
@ -303,7 +303,7 @@
}
.message.grouped {
padding: 0.25rem 1rem 0.25rem calc(1rem + 0.5rem + 31px);
padding: var(--pad-1) var(--pad-4) var(--pad-1) calc(var(--pad-4) + var(--pad-2) + 31px);
}
body:not(.sidebars_shown) .sidebar {

View file

@ -71,226 +71,12 @@
("class" "flex items-center")
("style" "color: var(--color-primary)")
(text "{{ icon \"square-asterisk\" }}"))
(text "{%- endif %}"))
(text "{% if user -%} {% if user.id != community.owner %}")
(div
("class" "dropdown")
(button
("class" "camo small")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown")
(text "{{ icon \"ellipsis\" }}"))
(div
("class" "inner")
(button
("class" "red")
("onclick" "trigger('me::report', ['{{ community.id }}', 'community'])")
(text "{{ icon \"flag\" }}")
(span
(text "{{ text \"general:action.report\" }}")))))
(text "{%- endif %} {%- endif %}"))
(text "{%- endif %}")))
(span
("class" "fade")
(text "{{ community.title }}"))))
(text "{% if user -%}")
(div
("class" "card flex gap-2 flex-wrap")
("id" "join_or_leave")
(text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}")
(button
("class" "primary")
("onclick" "join_community()")
(text "{{ icon \"circle-plus\" }}")
(span
(text "{{ text \"communities:action.join\" }}")))
(script
(text "globalThis.join_community = () => {
fetch(
\"/api/v1/communities/{{ community.id }}/join\",
{
method: \"POST\",
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};"))
(text "{% else %}")
(button
("class" "quaternary red")
("onclick" "cancel_request()")
(text "{{ icon \"x\" }}")
(span
(text "{{ text \"communities:action.cancel_request\" }}")))
(script
(text "globalThis.cancel_request = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(
\"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\",
{
method: \"DELETE\",
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};"))
(text "{%- endif %} {% else %}")
(button
("class" "quaternary red")
("onclick" "leave_community()")
(text "{{ icon \"circle-minus\" }}")
(span
(text "{{ text \"communities:action.leave\" }}")))
(a
("href" "/chats/{{ community.id }}/0")
("class" "button quaternary")
(text "{{ icon \"message-circle\" }}")
(span
(text "{{ text \"communities:label.chats\" }}")))
(text "{% if user and can_post -%}")
(a
("href" "/communities/intents/post?community={{ community.id }}")
("class" "button quaternary")
("data-turbo" "false")
(text "{{ icon \"plus\" }}")
(span
(text "{{ text \"general:action.post\" }}")))
(text "{%- endif %}")
(script
(text "globalThis.leave_community = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(
\"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\",
{
method: \"DELETE\",
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};"))
(text "{%- endif %} {% else %}")
(a
("href" "/chats/{{ community.id }}/0")
("class" "button quaternary")
(text "{{ icon \"message-circle\" }}")
(span
(text "{{ text \"communities:label.chats\" }}")))
(a
("href" "/communities/intents/post?community={{ community.id }}")
("class" "button quaternary")
("data-turbo" "false")
(text "{{ icon \"plus\" }}")
(span
(text "{{ text \"general:action.post\" }}")))
(text "{%- endif %} {% if can_manage_community or is_manager -%}")
(a
("href" "/community/{{ community.id }}/manage")
("class" "button primary")
(text "{{ icon \"settings\" }}")
(span
(text "{{ text \"communities:action.configure\" }}")))
(text "{%- endif %}"))
(text "{%- endif %}"))
(div
("class" "card-nest flex flex-col")
(div
("id" "bio")
("class" "card small no_p_margin")
(text "{{ community.context.description|markdown|safe }}"))
(div
("class" "card flex flex-col gap-2")
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "ID"))
(button
("title" "Copy")
("onclick" "trigger('atto::copy_text', ['{{ community.id }}'])")
("class" "camo small")
(text "{{ icon \"copy\" }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Created "))
(span
("class" "date")
(text "{{ community.created }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Members"))
(a
("href" "/community/{{ community.title }}/members")
(text "{{ community.member_count }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Score"))
(div
("class" "flex gap-2")
(b
(text "{{ community.likes - community.dislikes }}"))
(text "{% if user -%}")
(div
("class" "flex gap-1 reactions_box")
("hook" "check_reactions")
("hook-arg:id" "{{ community.id }}")
(text "{{ components::likes(id=community.id, asset_type=\"Community\", likes=community.likes, dislikes=community.dislikes) }}"))
(text "{%- endif %}")))
(text "{% if user and user.id != community.owner -%}")
(hr)
(div
("class" "flex flex-wrap gap-2 w-full fade")
(a
("class" "red")
("href" "javascript:trigger('me::report', ['{{ community.id }}', 'community'])")
(text "({{ lang[\"general:action.report\"]|lower }})")))
(text "{%- endif %}"))))
(text "{{ components::community_actions(community=community) }}"))
(text "{{ components::community_info(community=community) }}"))
(div
("class" "rhs w-full")
(text "{% if can_read -%} {% block content %}{% endblock %} {% else %}")
@ -307,4 +93,8 @@
(text "{{ text \"communities:label.might_need_to_join\" }}"))))
(text "{%- endif %}")))))
(text "{% if community.is_forge and not allow_for_forges %}")
(script
(text "window.location.pathname = window.location.pathname.replace(\"/community\", \"/forge\")"))
(text "{% endif %}")
(text "{% endblock %}")

View file

@ -66,10 +66,6 @@
e.preventDefault();
await trigger(\"atto::debounce\", [\"communities::create\"]);
if (e.target.title.value.includes(\" \")) {
return alert(\"Cannot contain spaces!\");
}
fetch(\"/api/v1/communities\", {
method: \"POST\",
headers: {

View file

@ -896,49 +896,56 @@
\"change_banner\",
]);
const settings_fields = [
[
[\"display_name\", \"Display title\"],
\"{{ community.context.display_name }}\",
\"input\",
],
[
[\"description\", \"Description\"],
settings.description,
\"textarea\",
],
[
[\"is_nsfw\", \"Mark as NSFW\"],
\"{{ community.context.is_nsfw }}\",
\"checkbox\",
]
];
// {% if not community.is_forge -%}
settings_fields.push([
[
\"enable_questions\",
\"Allow users to ask questions in this community\",
],
\"{{ community.context.enable_questions }}\",
\"checkbox\",
]);
settings_fields.push([
[
\"enable_titles\",
\"Allow users to attach a title to their posts\",
],
\"{{ community.context.enable_titles }}\",
\"checkbox\",
]);
settings_fields.push([
[
\"require_titles\",
\"Require users to attach a title to their posts\",
],
\"{{ community.context.require_titles }}\",
\"checkbox\",
]);
// {%- endif %}
ui.generate_settings_ui(
document.getElementById(\"manage_fields\"),
[
[
[\"display_name\", \"Display title\"],
\"{{ community.context.display_name }}\",
\"input\",
],
[
[\"description\", \"Description\"],
settings.description,
\"textarea\",
],
[
[\"is_nsfw\", \"Mark as NSFW\"],
\"{{ community.context.is_nsfw }}\",
\"checkbox\",
],
[
[
\"enable_questions\",
\"Allow users to ask questions in this community\",
],
\"{{ community.context.enable_questions }}\",
\"checkbox\",
],
[
[
\"enable_titles\",
\"Allow users to attach a title to their posts\",
],
\"{{ community.context.enable_titles }}\",
\"checkbox\",
],
[
[
\"require_titles\",
\"Require users to attach a title to their posts\",
],
\"{{ community.context.require_titles }}\",
\"checkbox\",
],
],
settings_fields,
settings,
);
}, 250);"))

View file

@ -49,13 +49,18 @@
(text "{%- endif %} {%- endmacro %} {% macro community_listing_card(community) -%}")
(a
("class" "card secondary w-full flex items-center gap-4")
("href" "/community/{{ community.title }}")
("href" "{% if community.is_forge -%}/forge/{{ community.title }}{% else %}/community/{{ community.title }}{%- endif %}")
(text "{{ self::community_avatar(id=community.id, community=community, size=\"48px\") }}")
(div
("class" "flex flex-col")
(h3
("class" "name lg:long")
(text "{{ community.context.display_name }}"))
(div
("class" "flex gap-2 items-center")
(text "{% if community.is_forge -%}")
(icon (text "anvil"))
(text "{%- endif %}")
(h3
("class" "name lg:long")
(text "{{ community.context.display_name }}")))
(span
("class" "fade")
(b
@ -1085,7 +1090,7 @@
--input-border-radiFus: var(--radius);
--input-border-color: var(--color-primary);
--indicator-color: var(--color-primary);
--emoji-padding: 0.25rem;
--emoji-padding: var(--pad-1);
box-shadow: 0 0 4px var(--color-shadow);
")
("class" "w-full"))
@ -1538,3 +1543,206 @@
("data-expires" "{{ poll[0].expires }}")))
(text "{%- endif %}")))
(text "{%- endmacro %}")
(text "{% macro community_info(community) %}")
(div
("class" "card-nest flex flex-col")
(div
("id" "bio")
("class" "card small no_p_margin")
(text "{{ community.context.description|markdown|safe }}"))
(div
("class" "card flex flex-col gap-2")
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "ID"))
(button
("title" "Copy")
("onclick" "trigger('atto::copy_text', ['{{ community.id }}'])")
("class" "camo small")
(text "{{ icon \"copy\" }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Created "))
(span
("class" "date")
(text "{{ community.created }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Members"))
(a
("href" "/community/{{ community.title }}/members")
(text "{{ community.member_count }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Posts"))
(a
("href" "/community/{{ community.title }}")
(text "{{ community.post_count }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Score"))
(div
("class" "flex gap-2")
(b
(text "{{ community.likes - community.dislikes }}"))
(text "{% if user -%}")
(div
("class" "flex gap-1 reactions_box")
("hook" "check_reactions")
("hook-arg:id" "{{ community.id }}")
(text "{{ components::likes(id=community.id, asset_type=\"Community\", likes=community.likes, dislikes=community.dislikes) }}"))
(text "{%- endif %}")))))
(text "{% endmacro %}")
(text "{% macro community_actions(community) -%}")
(text "{% if user -%}")
(div
("class" "card flex gap-2 flex-wrap")
("id" "join_or_leave")
(text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}")
(button
("class" "primary")
("onclick" "join_community()")
(text "{{ icon \"circle-plus\" }}")
(span
(text "{{ text \"communities:action.join\" }}")))
(script
(text "globalThis.join_community = () => {
fetch(
\"/api/v1/communities/{{ community.id }}/join\",
{
method: \"POST\",
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};"))
(text "{% else %}")
(button
("class" "quaternary red")
("onclick" "cancel_request()")
(text "{{ icon \"x\" }}")
(span
(text "{{ text \"communities:action.cancel_request\" }}")))
(script
(text "globalThis.cancel_request = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(
\"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\",
{
method: \"DELETE\",
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};"))
(text "{%- endif %} {% else %}")
(button
("class" "quaternary red")
("onclick" "leave_community()")
(text "{{ icon \"circle-minus\" }}")
(span
(text "{{ text \"communities:action.leave\" }}")))
(a
("href" "/chats/{{ community.id }}/0")
("class" "button quaternary")
(text "{{ icon \"message-circle\" }}")
(span
(text "{{ text \"communities:label.chats\" }}")))
(text "{% if user and can_post -%}")
(a
("href" "/communities/intents/post?community={{ community.id }}")
("class" "button quaternary")
("data-turbo" "false")
(text "{{ icon \"plus\" }}")
(span
(text "{{ text \"general:action.post\" }}")))
(text "{%- endif %}")
(script
(text "globalThis.leave_community = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(
\"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\",
{
method: \"DELETE\",
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};"))
(text "{%- endif %} {% else %}")
(a
("href" "/chats/{{ community.id }}/0")
("class" "button quaternary")
(text "{{ icon \"message-circle\" }}")
(span
(text "{{ text \"communities:label.chats\" }}")))
(a
("href" "/communities/intents/post?community={{ community.id }}")
("class" "button quaternary")
("data-turbo" "false")
(text "{{ icon \"plus\" }}")
(span
(text "{{ text \"general:action.post\" }}")))
(text "{%- endif %} {% if can_manage_community or is_manager -%}")
(a
("href" "/community/{{ community.id }}/manage")
("class" "button primary")
(text "{{ icon \"settings\" }}")
(span
(text "{{ text \"communities:action.configure\" }}")))
(text "{%- endif %}"))
(text "{%- endif %}")
(text "{%- endmacro %}")

View file

@ -0,0 +1,83 @@
; this is essentially the same as `communities/base.lisp`, but it has some minor
; changes to be more github-like instead of retrospring-like
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "{{ community.context.display_name }} - {{ config.name }}"))
(meta
("name" "og:title")
("content" "{{ community.title }}"))
(meta
("name" "description")
("content" "View the \\\"{{ community.title }}\\\" community on {{ config.name }}!"))
(meta
("name" "og:description")
("content" "View the \\\"{{ community.title }}\\\" community on {{ config.name }}!"))
(meta
("property" "og:type")
("content" "profile"))
(meta
("property" "profile:username")
("content" "{{ community.title }}"))
(meta
("name" "og:image")
("content" "{{ config.host|safe }}/api/v1/communities/{{ community.id }}/avatar"))
(meta
("name" "twitter:image")
("content" "{{ config.host|safe }}/api/v1/communities/{{ community.id }}/avatar"))
(meta
("name" "twitter:card")
("content" "summary"))
(meta
("name" "twitter:title")
("content" "{{ community.title }}"))
(meta
("name" "twitter:description")
("content" "View the \\\"{{ community.title }}\\\" community on {{ config.name }}!"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
(div
("class" "content_container flex flex-col gap-4")
(div
("class" "card-nest")
(div
("class" "card flex gap-2")
("id" "community_avatar_and_name")
(text "{{ components::community_avatar(id=community.id, community=community, size=\"72px\") }}")
(div
("class" "flex flex-col")
(div
("class" "flex gap-2 items-center")
(h3
("id" "title")
("class" "title name shorter flex gap-2")
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %} {% if community.context.is_nsfw -%}")
(span
("title" "NSFW community")
("class" "flex items-center")
("style" "color: var(--color-primary)")
(text "{{ icon \"square-asterisk\" }}"))
(text "{%- endif %}")))
(span
("class" "fade")
(text "{{ community.title }}"))))
(text "{{ components::community_actions(community=community) }}"))
(text "{% block content %}{% endblock %}")))
(text "{% if not community.is_forge %}")
(script
(text "window.location.pathname = window.location.pathname.replace(\"/forge\", \"/community\")"))
(text "{% endif %}")
(text "{% endblock %}")

View file

@ -0,0 +1,80 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Forge - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}")
(main
("class" "flex flex-col gap-2")
; create new
(text "{% if user.permissions|has_supporter -%}")
(div
("class" "card-nest")
(div
("class" "card small")
(b
(text "{{ text \"forge:label.create_new\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "create_community_from_form(event)")
(div
("class" "flex flex-col gap-1")
(label
("for" "title")
(text "{{ text \"communities:label.name\" }}"))
(input
("type" "text")
("name" "title")
("id" "title")
("placeholder" "name")
("required" "")
("minlength" "2")
("maxlength" "32")))
(button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))))
(text "{% else %}")
(text "{{ components::supporter_ad(body=\"Become a supporter to create forges!\") }}")
(text "{%- endif %}")
; forge listing
(div
("class" "card-nest")
(div
("class" "card small flex items-center gap-2")
(icon (text "anvil"))
(str (text "forge:label.my_forges")))
(div
("class" "card flex flex-col gap-2")
(text "{% for item in list %} {{ components::community_listing_card(community=item) }} {% endfor %}"))))
(script
(text "async function create_community_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"communities::create\"]);
fetch(\"/api/v1/communities\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title: e.target.title.value,
forge: true
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
setTimeout(() => {
window.location.href = `/forge/${res.payload}`;
}, 100);
}
});
}"))
(text "{% endblock %}")

View file

@ -0,0 +1,6 @@
(text "{% extends \"forge/base.html\" %} {% block content %}")
(div
("class" "flex flex-col gap-4 w-full")
(text "{{ macros::forge_nav(community=community, selected=\"info\") }}")
(text "{{ components::community_info(community=community) }}"))
(text "{% endblock %}")

View file

@ -1,7 +1,7 @@
(text "{% macro nav(selected=\"\", show_lhs=true, hide_user_menu=false) -%}")
(nav
(div
("class" "content_container")
("class" "content_container flex justify-between")
(div
("class" "flex nav_side")
(a
@ -72,7 +72,7 @@
("class" "flex-row title")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exlude" "dropdown")
("style" "gap: 0.25rem !important")
("style" "gap: var(--pad-1) !important")
(text "{{ components::avatar(username=user.username, size=\"24px\") }}")
(icon_class (text "chevron-down") (text "dropdown-arrow")))
@ -221,3 +221,19 @@
(str (text "auth:label.outbox")))
(text "{%- endif %} {%- endif %}"))
(text "{%- endmacro %}")
(text "{% macro forge_nav(community, selected=\"\") -%}")
(div
("class" "pillmenu")
(a
("href" "/forge/{{ community.title }}")
("class" "{% if selected == 'info' -%}active{%- endif %}")
(icon (text "info"))
(str (text "forge:tab.info")))
(a
("href" "/forge/{{ community.title }}/tickets")
("class" "{% if selected == 'tickets' -%}active{%- endif %}")
(icon (text "circle-dot"))
(str (text "forge:tab.tickets"))))
(text "{%- endmacro %}")

View file

@ -73,7 +73,7 @@
(style
(text ".user_plate {
width: calc(50% - 0.5rem);
width: calc(50% - var(--pad-2));
}
@media screen and (max-width: 900px) {

View file

@ -12,7 +12,7 @@
(style
(text ".user_plate {
width: calc(50% - 0.5rem);
width: calc(50% - var(--pad-2));
}
@media screen and (max-width: 900px) {

View file

@ -12,7 +12,7 @@
(style
(text ".user_plate {
width: calc(50% - 0.5rem);
width: calc(50% - var(--pad-2));
}
@media screen and (max-width: 900px) {

View file

@ -546,7 +546,7 @@
pressure, but it helps us do some pretty cool
things! As a supporter, you'll get:"))
(ul
("style" "margin-bottom: 1rem")
("style" "margin-bottom: var(--pad-4)")
(li
(text "Vanity badge on profile"))
(li
@ -569,7 +569,9 @@
(li
(text "Save infinite post drafts"))
(li
(text "Ability to search through all posts")))
(text "Ability to search through all posts"))
(li
(text "Ability to create forges")))
(a
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
("class" "button")

View file

@ -55,8 +55,6 @@
(text "{% block head %}{% endblock %}"))
(body
(div ("id" "toast_zone"))
(div
("id" "page")
(text "{% if user and user.id == 0 -%}")
@ -78,306 +76,4 @@
(text "{% else %} {% block body %}{% endblock %} {%- endif %}")
(text "<!-- html_footer_goes_here -->"))
; random js
(text "<script data-turbo-permanent=\"true\" id=\"init-script\">
document.documentElement.addEventListener(\"turbo:load\", () => {
const atto = ns(\"atto\");
if (!atto) {
window.location.reload();
return;
}
atto.disconnect_observers();
atto.remove_false_options();
atto.clean_date_codes();
atto.clean_poll_date_codes();
atto.link_filter();
atto[\"hooks::scroll\"](document.body, document.documentElement);
atto[\"hooks::dropdown.init\"](window);
atto[\"hooks::character_counter.init\"]();
atto[\"hooks::long_text.init\"]();
atto[\"hooks::alt\"]();
atto[\"hooks::online_indicator\"]();
atto[\"hooks::ips\"]();
atto[\"hooks::check_reactions\"]();
atto[\"hooks::tabs\"]();
atto[\"hooks::spotify_time_text\"](); // spotify durations
atto[\"hooks::verify_emoji\"]();
if (document.getElementById(\"tokens\")) {
trigger(\"me::render_token_picker\", [
document.getElementById(\"tokens\"),
]);
}
setTimeout(() => {
trigger(\"me::notifications_stream\");
}, 250);
});
</script>")
(text "{% if user -%}
<script data-turbo-permanent=\"true\" id=\"update-seen-script\">
document.documentElement.addEventListener(\"turbo:load\", () => {
trigger(\"me::seen\");
trigger(\"streams::user\", [\"{{ user.id }}\"]);
if (!window.location.pathname.startsWith(\"/chats/\")) {
if (window.socket) {
window.socket.send(\"Close\");
window.socket = undefined;
console.log(\"socket disconnect\");
}
}
});
</script>
{%- endif %}")
; dialogs
(dialog
("id" "link_filter")
(div
("class" "inner flex flex-col gap-2")
; warning stuff
(p (text "Pressing continue will bring you to the following URL:"))
(pre (code ("id" "link_filter_url")))
(p (text "Are sure you want to go there?"))
(hr ("class" "margin"))
(div
("class" "flex gap-2")
(a
("class" "button primary")
("id" "link_filter_continue")
("rel" "noopener noreferrer")
("target" "_blank")
("onclick", "document.getElementById('link_filter').close()")
(icon (text "external-link"))
(str (text "dialog:action.continue")))
(button
("class" "secondary")
("type" "button")
("onclick", "document.getElementById('link_filter').close()")
(icon (text "x"))
(str (text "dialog:action.cancel"))))))
(dialog
("id" "web_api_prompt")
(div
("class" "inner flex flex-col gap-2")
(form
("class" "flex gap-2 flex-col")
("onsubmit" "event.preventDefault()")
(label ("for" "prompt") ("id" "web_api_prompt:msg"))
(input ("id" "prompt") ("name" "prompt"))
(div
("class" "flex justify-between")
(div null?)
(div
("class" "flex gap-2")
(button
("class" "primary bold circle")
("onclick", "globalThis.web_api_prompt_submit(document.getElementById('prompt').value); document.getElementById('prompt').value = ''")
("type" "button")
(icon (text "check"))
(str (text "dialog:action.okay")))
(button
("class" "bold red camo")
("onclick", "globalThis.web_api_prompt_submit('')")
("type" "button")
(icon (text "x"))
(str (text "dialog:action.cancel"))))))))
(dialog
("id" "web_api_prompt_long")
(div
("class" "inner flex flex-col gap-2")
(form
("class" "flex gap-2 flex-col")
("onsubmit" "event.preventDefault()")
(label ("for" "prompt_long") ("id" "web_api_prompt_long:msg"))
(input ("id" "prompt_long") ("name" "prompt_long"))
(div
("class" "flex justify-between")
(div null?)
(div
("class" "flex gap-2")
(button
("class" "primary bold circle")
("onclick", "globalThis.web_api_prompt_long_submit(document.getElementById('prompt_long').value); document.getElementById('prompt_long').value = ''")
("type" "button")
(icon (text "check"))
(str (text "dialog:action.okay")))
(button
("class" "bold red camo")
("onclick", "globalThis.web_api_prompt_long_submit('')")
("type" "button")
(icon (text "x"))
(str (text "dialog:action.cancel"))))))))
(dialog
("id" "web_api_confirm")
(div
("class" "inner flex flex-col gap-2")
(form
("class" "flex gap-2 flex-col")
("onsubmit" "event.preventDefault()")
(span ("id" "web_api_confirm:msg"))
(div
("class" "flex justify-between")
(div null?)
(div
("class" "flex gap-2")
(button
("class" "primary bold circle")
("onclick", "globalThis.web_api_confirm_submit(true)")
("type" "button")
(icon (text "check"))
(str (text "dialog:action.yes")))
(button
("class" "bold red camo")
("onclick", "globalThis.web_api_confirm_submit(false)")
("type" "button")
(icon (text "x"))
(str (text "dialog:action.no"))))))))
(div
("class" "lightbox hidden")
("id" "lightbox")
(button
("class" "lightbox_exit small square quaternary red")
("onclick" "trigger('ui::lightbox_close')")
(icon (text "x")))
(a
("href" "")
("id" "lightbox_img_a")
("target" "_blank")
(img ("id" "lightbox_img") ("loading" "lazy"))))
; tokens dialog
(text "{% if user -%}")
(dialog
("id" "tokens_dialog")
(div
("class" "inner flex flex-col gap-2")
(form
("class" "flex gap-2 flex-col")
("onsubmit" "event.preventDefault()")
(div ("id" "tokens") ("style" "display: contents"))
(div
("class" "flex justify-between")
(a
("href" "/auth/login")
("class" "button")
("data-turbo", "false")
(icon (text "plus"))
(span (str (text "general:action.add_account"))))
(button
("class" "quaternary")
("onclick" "document.getElementById('tokens_dialog').close()")
("type" "button")
(icon (text "check")))))))
; user scripts
(text "{%- endif %} {% if user and use_user_theme -%} {{ components::theme(user=user, theme_preference=user.settings.theme_preference) }}
<script>
setTimeout(() => {
trigger(\"atto::use_theme_preference\");
}, 150);
</script>
{%- endif %} {% if user and user.connections.Spotify and config.connections.spotify_client_id and user.connections.Spotify[0].data.token and user.connections.Spotify[0].data.refresh_token %}
<script>
setTimeout(async () => {
if (window.spotify_init) {
return;
}
window.spotify_init = true;
const client_id = \"{{ config.connections.spotify_client_id }}\";
let token = \"{{ user.connections.Spotify[0].data.token }}\";
let refresh_token =
\"{{ user.connections.Spotify[0].data.refresh_token }}\";
if (token) {
// we already have a token
const pull_playing = async () => {
const playing = await trigger(\"spotify::get_playing\", [
token,
]);
if (playing.error) {
// refresh token
const [new_token, new_refresh_token, expires_in] =
await trigger(\"spotify::refresh_token\", [
client_id,
refresh_token,
]);
await trigger(\"connections::push_con_data\", [
\"Spotify\",
{
token: new_token,
expires_in: expires_in.toString(),
name: profile.display_name,
},
]);
token = new_token;
refresh_token = new_refresh_token;
return;
}
await trigger(\"spotify::publish_playing\", [playing]);
};
await pull_playing();
setInterval(pull_playing, 30_000);
} else {
window.spotify_needs_token = true;
}
}, 150);
</script>
{% elif user and user.connections.LastFm and config.connections.last_fm_key and user.connections.LastFm[0].data.session_token %}
<script>
setTimeout(async () => {
if (window.last_fm_init) {
return;
}
window.last_fm_init = true;
const user = \"{{ user.connections.LastFm[0].data.name }}\";
const session_token =
\"{{ user.connections.LastFm[0].data.session_token }}\";
if (session_token) {
// we already have a token
const pull_playing = async () => {
const playing = await trigger(\"last_fm::get_playing\", [
user,
session_token,
]);
await trigger(\"last_fm::publish_playing\", [playing]);
};
await pull_playing();
setInterval(pull_playing, 30_000);
} else {
window.last_fm_needs_token = true;
}
}, 150);
</script>
{%- endif %}")))
(text "{% include \"body.html\" %}")))