add: community settings ui

TODO: add community read/write access settings
TODO: add profile settings
TODO: profile following in ui
TODO: community joining and membership management in ui
This commit is contained in:
trisua 2025-03-29 22:27:57 -04:00
parent eecf357325
commit 6413ed09fb
20 changed files with 855 additions and 46 deletions

View file

@ -152,7 +152,14 @@ button svg {
}
hr {
border-top: 1px var(--color-super-lowered);
border-top: solid 1px var(--color-super-lowered) !important;
border-left: 0;
border-bottom: 0;
border-right: 0;
}
hr.margin {
margin: 1rem 0;
}
p,

View file

@ -25,6 +25,18 @@
</h3>
<span class="fade">{{ community.title }}</span>
{% if user %}
<div
class="flex gap-1 reactions_box"
hook="check_reactions"
hook-arg:id="{{ community.id }}"
>
{{ components::likes(id=community.id,
asset_type="Community", likes=community.likes,
dislikes=community.dislikes) }}
</div>
{% endif %}
</div>
</div>
@ -41,15 +53,86 @@
<span>{{ text "communities:action.leave" }}</span>
</button>
{% endif %} {% else %}
<a
href="/community/{{ community.title }}/manage"
<button
href="/community/{{ community.title }}"
class="button primary"
onclick="document.getElementById('manage').showModal()"
>
{{ icon "settings" }}
<span
>{{ text "communities:action.configure" }}</span
>
</a>
</button>
<dialog id="manage">
<div class="inner">
<div
id="manage_fields"
class="flex flex-col gap-2"
></div>
<hr class="margin" />
<button
onclick="document.getElementById('manage').close(); save_context()"
>
{{ icon "check" }}
<span
>{{ text "dialog:action.save_and_close"
}}</span
>
</button>
</div>
</dialog>
<script>
setTimeout(() => {
const ui = ns("ui");
const settings = JSON.parse(
"{{ community_context_serde|safe }}",
);
ui.generate_settings_ui(
document.getElementById("manage_fields"),
[
[
["display_name", "Title"],
"{{ community.context.display_name }}",
"input",
],
[
["description", "Description"],
"{{ community.context.description }}",
"textarea",
],
],
settings,
);
window.save_context = () => {
fetch(
`/api/v1/communities/{{ community.id }}/context`,
{
method: "POST",
headers: {
"Content-Type":
"application/json",
},
body: JSON.stringify({
context: settings,
}),
},
)
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
};
}, 250);
</script>
{% endif %}
</div>
{% endif %}
@ -57,7 +140,7 @@
<div class="card-nest flex flex-col">
<div id="bio" class="card small">
{{ community.context.description }}
{{ community.context.description|markdown|safe }}
</div>
<div class="card flex flex-col gap-2">

View file

@ -38,6 +38,28 @@
{% if user.settings.display_name %} {{ user.settings.display_name }} {% else
%} {{ user.username }} {% endif %}
</div>
{%- endmacro %} {% macro likes(id, asset_type, likes=0, dislikes=0) -%}
<button
title="Like"
class="camo small"
hook_element="reaction.like"
onclick="trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', true])"
>
{{ icon "heart" }} {% if likes > 0 %}
<span>{{ likes }}</span>
{% endif %}
</button>
<button
title="Dislike"
class="camo small"
hook_element="reaction.dislike"
onclick="trigger('me::react', [event.target, '{{ id }}', '{{ asset_type }}', false])"
>
{{ icon "heart-crack" }} {% if dislikes > 0 %}
<span>{{ dislikes }}</span>
{% endif %}
</button>
{%- endmacro %} {% macro post(post, owner, secondary=false, community=false,
show_community=true) -%}
<div class="card flex flex-col gap-2 {% if secondary %}secondary{% endif %}">
@ -68,21 +90,23 @@ show_community=true) -%}
{% endif %}
</div>
<span id="post-content:{{ post.id }}">{{ post.content }}</span>
<span id="post-content:{{ post.id }}"
>{{ post.content|markdown|safe }}</span
>
</div>
</div>
<div class="flex justify-between items-center gap-2 w-full">
<div class="flex gap-1 reactions_box">
{% if user %}
<button title="Like" class="primary small">
{{ icon "heart" }}
</button>
<button title="Dislike" class="secondary small">
{{ icon "heart-crack" }}
</button>
{% endif %}
{% if user %}
<div
class="flex gap-1 reactions_box"
hook="check_reactions"
hook-arg:id="{{ post.id }}"
>
{{ components::likes(id=post.id, asset_type="Post",
likes=post.likes, dislikes=post.dislikes) }}
</div>
{% endif %}
<div class="flex gap-1 buttons_box">
<a href="/post/{{ post.id }}" class="button camo small">

View file

@ -53,7 +53,7 @@
<div class="card-nest flex flex-col">
<div id="bio" class="card small">
{{ profile.settings.biography }}
{{ profile.settings.biography|markdown|safe }}
</div>
<div class="card flex flex-col gap-2">

View file

@ -455,21 +455,33 @@ media_theme_pref();
self.define("hooks::check_reactions", async ({ $ }) => {
const observer = $.offload_work_to_client_when_in_view(
async (element) => {
const like = element.querySelector(
'[hook_element="reaction.like"]',
);
const dislike = element.querySelector(
'[hook_element="reaction.dislike"]',
);
const reaction = await (
await fetch(
`/api/v1/reactions/${element.getAttribute("hook-arg:id")}`,
)
).json();
if (reaction.success) {
element.classList.add("green");
element.querySelector("svg").classList.add("filled");
if (reaction.ok) {
if (reaction.payload.is_like) {
like.classList.add("green");
like.querySelector("svg").classList.add("filled");
} else {
dislike.classList.add("red");
}
}
},
);
for (const element of Array.from(
document.querySelectorAll("[hook=check_reaction]") || [],
document.querySelectorAll("[hook=check_reactions]") || [],
)) {
observer.observe(element);
}
@ -619,3 +631,44 @@ media_theme_pref();
}
});
})();
// ui ns
(() => {
const self = reg_ns("ui");
self.define("render_settings_ui_field", (_, into_element, option) => {
into_element.innerHTML += `<div class="card-nest">
<div class="card small">
<b>${option.label.replaceAll("_", " ")}</b>
</div>
<div class="card">
<${option.input_element_type || "input"}
type="text"
onchange="window.set_setting_field('${option.key}', event.target.value)"
placeholder="${option.key}"
${option.input_element_type === "input" ? `value="${option.value}"/>` : ">"}
${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
</div>
</div>`;
});
self.define(
"generate_settings_ui",
({ $ }, into_element, options, settings_ref) => {
for (const option of options) {
$.render_settings_ui_field(into_element, {
key: Array.isArray(option[0]) ? option[0][0] : option[0],
label: Array.isArray(option[0]) ? option[0][1] : option[0],
value: option[1],
input_element_type: option[2],
});
}
window.set_setting_field = (key, value) => {
settings_ref[key] = value;
console.log("update", key);
};
},
);
})();

View file

@ -48,4 +48,47 @@
]);
});
});
self.define("react", async (_, element, asset, asset_type, is_like) => {
fetch("/api/v1/reactions", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
asset,
asset_type,
is_like,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (res.ok) {
const like = element.parentElement.querySelector(
'[hook_element="reaction.like"]',
);
const dislike = element.parentElement.querySelector(
'[hook_element="reaction.dislike"]',
);
if (is_like) {
like.classList.add("green");
like.querySelector("svg").classList.add("filled");
dislike.classList.remove("red");
} else {
dislike.classList.add("red");
like.classList.remove("green");
like.querySelector("svg").classList.remove("filled");
}
}
});
});
})();