add: forges ui
TODO: forges tickets feed, posts open/closed state
This commit is contained in:
parent
5b1db42c51
commit
a6140f7c8c
40 changed files with 1664 additions and 1270 deletions
58
Cargo.lock
generated
58
Cargo.lock
generated
|
@ -1109,9 +1109,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
@ -2027,9 +2027,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oiseau"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8082a9c49b3d0cf132dcfdeba5b3c8a21c3a21a98623fc1f8571e6fc3956ce38"
|
||||
checksum = "99b097052e28781d560587373845626a85460969a55d180fc418aecd58f6fef3"
|
||||
dependencies = [
|
||||
"bb8-postgres",
|
||||
"redis",
|
||||
|
@ -2662,9 +2662,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.18"
|
||||
version = "0.12.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5"
|
||||
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
|
@ -2957,9 +2957,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
@ -3061,9 +3061,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.0"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "smart-default"
|
||||
|
@ -3302,7 +3302,6 @@ dependencies = [
|
|||
"image",
|
||||
"mime_guess",
|
||||
"pathbufd",
|
||||
"redis",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
@ -3582,9 +3581,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.22"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
|
@ -3594,18 +3593,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.9"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.26"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
|
@ -3617,9 +3616,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "totp-rs"
|
||||
|
@ -3656,12 +3655,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.4"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
|
@ -4147,9 +4147,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
|
|||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.5.2"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
|
||||
checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
|
||||
dependencies = [
|
||||
"redox_syscall",
|
||||
"wasite",
|
||||
|
@ -4445,9 +4445,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
|||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.7"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -4493,18 +4493,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.23"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
|
||||
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.23"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
|
||||
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -6,7 +6,7 @@ edition = "2024"
|
|||
[features]
|
||||
postgres = ["tetratto-core/postgres"]
|
||||
sqlite = ["tetratto-core/sqlite"]
|
||||
redis = ["tetratto-core/redis", "dep:redis"]
|
||||
redis = ["tetratto-core/redis"]
|
||||
default = ["sqlite", "redis"]
|
||||
|
||||
[dependencies]
|
||||
|
@ -15,7 +15,7 @@ serde = { version = "1.0.219", features = ["derive"] }
|
|||
tera = "1.20.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tower-http = { version = "0.6.4", features = ["trace", "fs", "catch-panic"] }
|
||||
tower-http = { version = "0.6.6", features = ["trace", "fs", "catch-panic"] }
|
||||
axum = { version = "0.8.4", features = ["macros", "ws"] }
|
||||
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
|
||||
axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
|
||||
|
@ -27,18 +27,13 @@ tetratto-core = { path = "../core", features = [
|
|||
tetratto-l10n = { path = "../l10n" }
|
||||
|
||||
image = "0.25.6"
|
||||
reqwest = { version = "0.12.18", features = ["json", "stream"] }
|
||||
reqwest = { version = "0.12.19", features = ["json", "stream"] }
|
||||
regex = "1.11.1"
|
||||
serde_json = "1.0.140"
|
||||
mime_guess = "2.0.5"
|
||||
cf-turnstile = "0.2.0"
|
||||
contrasted = "0.1.3"
|
||||
futures-util = "0.3.31"
|
||||
|
||||
redis = { version = "0.31.0", features = [
|
||||
"aio",
|
||||
"tokio-comp",
|
||||
], optional = true }
|
||||
async-stripe = { version = "0.41.0", features = [
|
||||
"events",
|
||||
"checkout",
|
||||
|
|
|
@ -31,6 +31,8 @@ pub const FAVICON: &str = include_str!("./public/images/favicon.svg");
|
|||
|
||||
// css
|
||||
pub const STYLE_CSS: &str = include_str!("./public/css/style.css");
|
||||
pub const ROOT_CSS: &str = include_str!("./public/css/root.css");
|
||||
pub const UTILITY_CSS: &str = include_str!("./public/css/utility.css");
|
||||
|
||||
// js
|
||||
pub const LOADER_JS: &str = include_str!("./public/js/loader.js");
|
||||
|
@ -39,6 +41,7 @@ pub const ME_JS: &str = include_str!("./public/js/me.js");
|
|||
pub const STREAMS_JS: &str = include_str!("./public/js/streams.js");
|
||||
|
||||
// html
|
||||
pub const BODY: &str = include_str!("./public/html/body.lisp");
|
||||
pub const ROOT: &str = include_str!("./public/html/root.lisp");
|
||||
pub const MACROS: &str = include_str!("./public/html/macros.lisp");
|
||||
pub const COMPONENTS: &str = include_str!("./public/html/components.lisp");
|
||||
|
@ -113,6 +116,10 @@ pub const STACKS_LIST: &str = include_str!("./public/html/stacks/list.lisp");
|
|||
pub const STACKS_POSTS: &str = include_str!("./public/html/stacks/posts.lisp");
|
||||
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");
|
||||
|
||||
// langs
|
||||
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
|
||||
|
||||
|
@ -312,6 +319,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
|||
let mut plugins = lisp_plugins();
|
||||
let html_path = PathBufD::current().join(&config.dirs.templates);
|
||||
|
||||
write_template!(html_path->"body.html"(crate::assets::BODY) --config=config --lisp plugins);
|
||||
write_template!(html_path->"root.html"(crate::assets::ROOT) --config=config --lisp plugins);
|
||||
write_template!(html_path->"macros.html"(crate::assets::MACROS) --config=config --lisp plugins);
|
||||
write_template!(html_path->"components.html"(crate::assets::COMPONENTS) --config=config --lisp plugins);
|
||||
|
@ -381,6 +389,10 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
|||
write_template!(html_path->"stacks/posts.html"(crate::assets::STACKS_POSTS) --config=config --lisp plugins);
|
||||
write_template!(html_path->"stacks/manage.html"(crate::assets::STACKS_MANAGE) --config=config --lisp plugins);
|
||||
|
||||
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);
|
||||
|
||||
html_path
|
||||
}
|
||||
|
||||
|
|
|
@ -203,3 +203,8 @@ version = "1.0.0"
|
|||
"stacks:tab.users" = "Users"
|
||||
"stacks:label.add_user" = "Add user"
|
||||
"stacks:label.remove" = "Remove"
|
||||
|
||||
"forge:label.my_forges" = "My forges"
|
||||
"forge:label.create_new" = "Create new forge"
|
||||
"forge:tab.info" = "Info"
|
||||
"forge:tab.tickets" = "Tickets"
|
||||
|
|
344
crates/app/src/public/css/root.css
Normal file
344
crates/app/src/public/css/root.css
Normal file
|
@ -0,0 +1,344 @@
|
|||
@import url("utility.css");
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--hue: 16;
|
||||
--sat: 6%;
|
||||
--lit: 0%;
|
||||
--color-surface: hsl(var(--hue), var(--sat), calc(97% - var(--lit)));
|
||||
--color-lowered: hsl(var(--hue), var(--sat), calc(94% - var(--lit)));
|
||||
--color-raised: hsl(var(--hue), var(--sat), calc(99% - var(--lit)));
|
||||
--color-super-lowered: hsl(var(--hue), var(--sat), calc(85% - var(--lit)));
|
||||
--color-super-raised: hsl(var(--hue), var(--sat), calc(100% - var(--lit)));
|
||||
--color-text: hsl(0, 0%, 0%);
|
||||
--color-text-raised: var(--color-text);
|
||||
--color-text-lowered: var(--color-text);
|
||||
|
||||
--color-primary: hsl(330, 18%, 26%);
|
||||
--color-primary-lowered: hsl(330, 18%, 21%);
|
||||
--color-text-primary: hsl(0, 0%, 100%);
|
||||
|
||||
--color-secondary: hsl(6, 18%, 66%);
|
||||
--color-secondary-lowered: hsl(6, 18%, 61%);
|
||||
--color-text-secondary: hsl(0, 0%, 0%);
|
||||
|
||||
--color-link: #2949b2;
|
||||
--color-shadow: rgba(0, 0, 0, 0.08);
|
||||
--color-red: hsl(0, 84%, 40%);
|
||||
--color-green: hsl(100, 84%, 20%);
|
||||
--color-yellow: hsl(41, 63%, 75%);
|
||||
--radius: 6px;
|
||||
--circle: 360px;
|
||||
--shadow-x-offset: 0;
|
||||
--shadow-y-offset: 0.125rem;
|
||||
--shadow-size: var(--pad-1);
|
||||
|
||||
--pad-1: 0.25rem;
|
||||
--pad-2: 0.5rem;
|
||||
--pad-3: 0.75rem;
|
||||
--pad-4: 1rem;
|
||||
}
|
||||
|
||||
.dark,
|
||||
.dark * {
|
||||
--hue: 266;
|
||||
--sat: 14%;
|
||||
--lit: 12%;
|
||||
--color-surface: hsl(var(--hue), var(--sat), calc(0% + var(--lit)));
|
||||
--color-lowered: hsl(var(--hue), var(--sat), calc(6% + var(--lit)));
|
||||
--color-raised: hsl(var(--hue), var(--sat), calc(2% + var(--lit)));
|
||||
--color-super-lowered: hsl(var(--hue), var(--sat), calc(12% + var(--lit)));
|
||||
--color-super-raised: hsl(var(--hue), var(--sat), calc(4% + var(--lit)));
|
||||
--color-text: hsl(0, 0%, 95%);
|
||||
|
||||
--color-primary: hsl(331, 18%, 74%);
|
||||
--color-primary-lowered: hsl(331, 18%, 69%);
|
||||
--color-text-primary: hsl(0, 0%, 0%);
|
||||
|
||||
--color-secondary: hsl(6, 18%, 34%);
|
||||
--color-secondary-lowered: hsl(6, 18%, 29%);
|
||||
--color-text-secondary: hsl(0, 0%, 100%);
|
||||
|
||||
--color-link: #93c5fd;
|
||||
--color-red: hsl(0, 94%, 82%);
|
||||
--color-green: hsl(100, 94%, 82%);
|
||||
--color-yellow: hsl(41, 63%, 65%);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.15px;
|
||||
font-family:
|
||||
"Inter", "Poppins", "Roboto", ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
overflow: auto auto;
|
||||
height: 100dvh;
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 80ch;
|
||||
margin: 0 auto;
|
||||
padding: var(--pad-3) var(--pad-4);
|
||||
}
|
||||
|
||||
article {
|
||||
margin: var(--pad-4) 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
main,
|
||||
article,
|
||||
nav,
|
||||
header,
|
||||
footer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
article {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body .card:not(.card *):not(#stream *):not(.user_plate),
|
||||
body .pillmenu:not(.card *) > a,
|
||||
body .card-nest:not(.card *) > .card,
|
||||
body .banner {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.content_container {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 500px) {
|
||||
.content_container {
|
||||
max-width: 540px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.content_container {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.content_container {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
article {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content_container {
|
||||
max-width: 1100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100%;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* typo */
|
||||
p {
|
||||
margin-bottom: var(--pad-4);
|
||||
}
|
||||
|
||||
.no_p_margin p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.post_content pre,
|
||||
.post_content h1,
|
||||
.post_content h2,
|
||||
.post_content h3 {
|
||||
max-width: calc(100% - 39px - var(--pad-2));
|
||||
}
|
||||
|
||||
.post_right:not(.repost) {
|
||||
max-width: calc(100% - 52px);
|
||||
}
|
||||
|
||||
.rhs {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.name {
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
/* overflow-wrap: break-word; */
|
||||
overflow-wrap: anywhere;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 901px) {
|
||||
.name.shorter {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.name.lg\:long {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.rhs {
|
||||
width: calc(100% - 23rem) !important;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: var(--pad-4);
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-family: "Jetbrains Mono", "Fire Code", monospace;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
background: var(--color-lowered);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--pad-1);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: var(--pad-4);
|
||||
}
|
||||
|
||||
svg.icon {
|
||||
stroke: currentColor;
|
||||
width: 18px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
svg.icon.filled {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
button svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: solid 1px var(--color-super-lowered) !important;
|
||||
border-left: 0;
|
||||
border-bottom: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
hr.margin {
|
||||
margin: var(--pad-4) 0;
|
||||
}
|
||||
|
||||
p,
|
||||
li,
|
||||
span,
|
||||
code {
|
||||
max-width: 100%;
|
||||
overflow-wrap: normal;
|
||||
text-wrap: pretty;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: var(--pad-4);
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: var(--pad-3);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
a.flush {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: inline;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
img.cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
img.fill {
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
img.contain {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
|
@ -1,340 +1,4 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
--hue: 16;
|
||||
--sat: 6%;
|
||||
--lit: 0%;
|
||||
--color-surface: hsl(var(--hue), var(--sat), calc(97% - var(--lit)));
|
||||
--color-lowered: hsl(var(--hue), var(--sat), calc(94% - var(--lit)));
|
||||
--color-raised: hsl(var(--hue), var(--sat), calc(99% - var(--lit)));
|
||||
--color-super-lowered: hsl(var(--hue), var(--sat), calc(85% - var(--lit)));
|
||||
--color-super-raised: hsl(var(--hue), var(--sat), calc(100% - var(--lit)));
|
||||
--color-text: hsl(0, 0%, 0%);
|
||||
--color-text-raised: var(--color-text);
|
||||
--color-text-lowered: var(--color-text);
|
||||
|
||||
--color-primary: hsl(330, 18%, 26%);
|
||||
--color-primary-lowered: hsl(330, 18%, 21%);
|
||||
--color-text-primary: hsl(0, 0%, 100%);
|
||||
|
||||
--color-secondary: hsl(6, 18%, 66%);
|
||||
--color-secondary-lowered: hsl(6, 18%, 61%);
|
||||
--color-text-secondary: hsl(0, 0%, 0%);
|
||||
|
||||
--color-link: #2949b2;
|
||||
--color-shadow: rgba(0, 0, 0, 0.08);
|
||||
--color-red: hsl(0, 84%, 40%);
|
||||
--color-green: hsl(100, 84%, 20%);
|
||||
--color-yellow: hsl(41, 63%, 75%);
|
||||
--radius: 6px;
|
||||
--circle: 360px;
|
||||
--shadow-x-offset: 0;
|
||||
--shadow-y-offset: 0.125rem;
|
||||
--shadow-size: 0.25rem;
|
||||
}
|
||||
|
||||
.dark,
|
||||
.dark * {
|
||||
--hue: 266;
|
||||
--sat: 14%;
|
||||
--lit: 12%;
|
||||
--color-surface: hsl(var(--hue), var(--sat), calc(0% + var(--lit)));
|
||||
--color-lowered: hsl(var(--hue), var(--sat), calc(6% + var(--lit)));
|
||||
--color-raised: hsl(var(--hue), var(--sat), calc(2% + var(--lit)));
|
||||
--color-super-lowered: hsl(var(--hue), var(--sat), calc(12% + var(--lit)));
|
||||
--color-super-raised: hsl(var(--hue), var(--sat), calc(4% + var(--lit)));
|
||||
--color-text: hsl(0, 0%, 95%);
|
||||
|
||||
--color-primary: hsl(331, 18%, 74%);
|
||||
--color-primary-lowered: hsl(331, 18%, 69%);
|
||||
--color-text-primary: hsl(0, 0%, 0%);
|
||||
|
||||
--color-secondary: hsl(6, 18%, 34%);
|
||||
--color-secondary-lowered: hsl(6, 18%, 29%);
|
||||
--color-text-secondary: hsl(0, 0%, 100%);
|
||||
|
||||
--color-link: #93c5fd;
|
||||
--color-red: hsl(0, 94%, 82%);
|
||||
--color-green: hsl(100, 94%, 82%);
|
||||
--color-yellow: hsl(41, 63%, 65%);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.15px;
|
||||
font-family:
|
||||
"Inter", "Poppins", "Roboto", ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
overflow: auto auto;
|
||||
height: 100dvh;
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 80ch;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
article {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
main,
|
||||
article,
|
||||
nav,
|
||||
header,
|
||||
footer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
article {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body .card:not(.card *):not(#stream *):not(.user_plate),
|
||||
body .pillmenu:not(.card *) > a,
|
||||
body .card-nest:not(.card *) > .card,
|
||||
body .banner {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.content_container {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 500px) {
|
||||
.content_container {
|
||||
max-width: 540px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.content_container {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.content_container {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
article {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content_container {
|
||||
max-width: 1100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100%;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* typo */
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.no_p_margin p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.post_content pre,
|
||||
.post_content h1,
|
||||
.post_content h2,
|
||||
.post_content h3 {
|
||||
max-width: calc(100% - 39px - 0.5rem);
|
||||
}
|
||||
|
||||
.post_right:not(.repost) {
|
||||
max-width: calc(100% - 52px);
|
||||
}
|
||||
|
||||
.rhs {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.name {
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
/* overflow-wrap: break-word; */
|
||||
overflow-wrap: anywhere;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 901px) {
|
||||
.name.shorter {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.name.lg\:long {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.rhs {
|
||||
width: calc(100% - 23rem) !important;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-family: "Jetbrains Mono", "Fire Code", monospace;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
background: var(--color-lowered);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
svg.icon {
|
||||
stroke: currentColor;
|
||||
width: 18px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
svg.icon.filled {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
button svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: solid 1px var(--color-super-lowered) !important;
|
||||
border-left: 0;
|
||||
border-bottom: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
hr.margin {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
p,
|
||||
li,
|
||||
span,
|
||||
code {
|
||||
max-width: 100%;
|
||||
overflow-wrap: normal;
|
||||
text-wrap: pretty;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
a.flush {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: inline;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
img.cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
img.fill {
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
img.contain {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
@import url("root.css");
|
||||
|
||||
.media_gallery {
|
||||
display: grid;
|
||||
|
@ -380,8 +44,8 @@ img.emoji {
|
|||
}
|
||||
|
||||
.lightbox_exit {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
top: var(--pad-4);
|
||||
right: var(--pad-4);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
@ -504,7 +168,7 @@ table ol {
|
|||
|
||||
/* card */
|
||||
.card {
|
||||
padding: 1rem;
|
||||
padding: var(--pad-4);
|
||||
background: var(--color-raised);
|
||||
color: var(--color-text-raised);
|
||||
box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size)
|
||||
|
@ -513,11 +177,11 @@ table ol {
|
|||
}
|
||||
|
||||
.card.small {
|
||||
padding: 0.5rem 1rem;
|
||||
padding: var(--pad-2) var(--pad-4);
|
||||
}
|
||||
|
||||
.card.tiny {
|
||||
padding: 0.5rem;
|
||||
padding: var(--pad-2);
|
||||
}
|
||||
|
||||
.card.secondary {
|
||||
|
@ -591,13 +255,13 @@ button,
|
|||
transition: background 0.15s;
|
||||
width: max-content;
|
||||
height: 32px;
|
||||
padding: 0.25rem 1rem;
|
||||
padding: var(--pad-1) var(--pad-4);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: var(--pad-1);
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none !important;
|
||||
user-select: none;
|
||||
|
@ -610,7 +274,7 @@ button,
|
|||
button.small,
|
||||
.button.small {
|
||||
/* min-height: max-content; */
|
||||
padding: 0.25rem 0.5rem;
|
||||
padding: var(--pad-1) var(--pad-2);
|
||||
height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
@ -704,7 +368,7 @@ button.camo:hover,
|
|||
input,
|
||||
textarea,
|
||||
select {
|
||||
padding: 0.35rem 0.75rem;
|
||||
padding: 0.35rem var(--pad-3);
|
||||
border-radius: var(--radius);
|
||||
border: solid 1px var(--color-super-lowered);
|
||||
outline: none;
|
||||
|
@ -752,7 +416,7 @@ select:focus {
|
|||
|
||||
.pillmenu a {
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
padding: var(--pad-2) var(--pad-4);
|
||||
width: 100%;
|
||||
color: var(--color-text-raised);
|
||||
background: var(--color-super-raised);
|
||||
|
@ -760,7 +424,7 @@ select:focus {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
gap: var(--pad-2);
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -887,7 +551,7 @@ select:focus {
|
|||
color: var(--color-text-primary);
|
||||
font-weight: 600;
|
||||
border-radius: var(--circle);
|
||||
padding: 0.05rem 0.75rem;
|
||||
padding: 0.05rem var(--pad-3);
|
||||
}
|
||||
|
||||
/* nav */
|
||||
|
@ -902,7 +566,7 @@ nav {
|
|||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 6374;
|
||||
padding: 0.25rem 0.5rem;
|
||||
padding: var(--pad-1) var(--pad-2);
|
||||
transition: opacity 0.15s;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
@ -922,7 +586,7 @@ nav button:not(.inner *),
|
|||
nav .button:not(.inner *) {
|
||||
border-radius: var(--radius);
|
||||
color: inherit;
|
||||
padding: 0.75rem 0.75rem;
|
||||
padding: var(--pad-3) var(--pad-3);
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
|
@ -953,7 +617,7 @@ nav .button:not(.title):not(.active):hover {
|
|||
|
||||
@media screen and (max-width: 900px) {
|
||||
nav {
|
||||
padding: 0.5rem 0.25rem;
|
||||
padding: var(--pad-2) var(--pad-1);
|
||||
margin-bottom: 0;
|
||||
backdrop-filter: none;
|
||||
bottom: 0;
|
||||
|
@ -1024,7 +688,7 @@ dialog {
|
|||
}
|
||||
|
||||
dialog .inner {
|
||||
padding: 1rem;
|
||||
padding: var(--pad-4);
|
||||
width: 25rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
@ -1067,7 +731,7 @@ dialog::backdrop {
|
|||
max-width: 100dvw;
|
||||
max-height: 80dvh;
|
||||
overflow: auto;
|
||||
padding: 0.5rem 0;
|
||||
padding: var(--pad-2) 0;
|
||||
box-shadow: 0 0 8px 2px var(--color-shadow);
|
||||
}
|
||||
|
||||
|
@ -1087,7 +751,7 @@ dialog::backdrop {
|
|||
}
|
||||
|
||||
.dropdown .inner .title {
|
||||
padding: 0.25rem var(--horizontal-padding);
|
||||
padding: var(--pad-1) var(--horizontal-padding);
|
||||
font-size: 13px;
|
||||
opacity: 50%;
|
||||
color: var(--color-text-raised);
|
||||
|
@ -1099,19 +763,19 @@ dialog::backdrop {
|
|||
}
|
||||
|
||||
.dropdown .inner .title:not(:first-of-type) {
|
||||
padding-top: 0.5rem;
|
||||
padding-top: var(--pad-2);
|
||||
}
|
||||
|
||||
.dropdown .inner a,
|
||||
.dropdown .inner button {
|
||||
width: 100%;
|
||||
padding: 0.25rem var(--horizontal-padding);
|
||||
padding: var(--pad-1) var(--horizontal-padding);
|
||||
transition: none !important;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
gap: var(--pad-2);
|
||||
color: var(--color-text-raised);
|
||||
box-shadow: none !important;
|
||||
background: transparent;
|
||||
|
@ -1163,25 +827,25 @@ dialog::backdrop {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.25rem;
|
||||
gap: var(--pad-1);
|
||||
position: fixed;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
bottom: var(--pad-2);
|
||||
right: var(--pad-2);
|
||||
z-index: 6880;
|
||||
width: calc(100% - 1rem);
|
||||
width: calc(100% - var(--pad-4));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
box-shadow: 0 0 8px var(--color-shadow);
|
||||
width: max-content;
|
||||
max-width: calc(100dvw - 1rem);
|
||||
max-width: calc(100dvw - var(--pad-4));
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 1rem;
|
||||
padding: var(--pad-3) var(--pad-4);
|
||||
animation: popin ease-in-out 1 0.15s running;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
gap: var(--pad-4);
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
|
@ -1275,7 +939,7 @@ dialog::backdrop {
|
|||
position: absolute;
|
||||
content: "Show full content";
|
||||
border-radius: var(--radius);
|
||||
padding: 0.25rem 0.75rem;
|
||||
padding: var(--pad-1) var(--pad-3);
|
||||
background: var(--color-primary);
|
||||
font-weight: 600;
|
||||
bottom: 20px;
|
||||
|
@ -1306,20 +970,15 @@ dialog::backdrop {
|
|||
}
|
||||
}
|
||||
|
||||
/* turbo */
|
||||
.turbo-progress-bar {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
/* details */
|
||||
details summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: var(--pad-1);
|
||||
transition: background 0.15s;
|
||||
cursor: pointer;
|
||||
width: max-content;
|
||||
padding: 0.25rem 0.75rem;
|
||||
padding: var(--pad-1) var(--pad-3);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-lowered);
|
||||
}
|
||||
|
@ -1336,7 +995,7 @@ details[open] summary {
|
|||
position: relative;
|
||||
color: var(--color-primary);
|
||||
background: var(--color-super-lowered);
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: var(--pad-1);
|
||||
}
|
||||
|
||||
details[open] summary::after {
|
||||
|
@ -1365,7 +1024,7 @@ details.accordion summary {
|
|||
background: var(--background);
|
||||
border: solid 1px var(--color-super-lowered);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 1rem;
|
||||
padding: var(--pad-3) var(--pad-4);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
|
@ -1386,219 +1045,10 @@ details.accordion[open] summary {
|
|||
|
||||
details.accordion .inner {
|
||||
background: var(--background);
|
||||
padding: 0.75rem 1rem;
|
||||
padding: var(--pad-3) var(--pad-4);
|
||||
border-radius: var(--radius);
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border: solid 1px var(--color-super-lowered);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* utility */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-rev-col {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.flex-rev-row {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-right {
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gap-1 {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gap-8 {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.flex-collapse {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.sm\:static {
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
.mobile.flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.sm\:w-full {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.sm\:mt-2 {
|
||||
margin-top: 2rem !important;
|
||||
}
|
||||
|
||||
.sm\:items-start {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.sm\:contents {
|
||||
display: contents !important;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0 8px var(--color-shadow);
|
||||
}
|
||||
|
||||
.shadow-md {
|
||||
box-shadow: 0 8px 16px var(--color-shadow);
|
||||
}
|
||||
|
||||
.round-sm {
|
||||
border-radius: calc(var(--radius) / 2) !important;
|
||||
}
|
||||
|
||||
.round {
|
||||
border-radius: var(--radius) !important;
|
||||
}
|
||||
|
||||
.round-md {
|
||||
border-radius: calc(var(--radius) * 2) !important;
|
||||
}
|
||||
|
||||
.round-lg {
|
||||
border-radius: calc(var(--radius) * 4) !important;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.w-content {
|
||||
width: max-content !important;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[disabled="fully"] {
|
||||
opacity: 75%;
|
||||
pointer-events: visible;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fade,
|
||||
.CodeMirror-placeholder {
|
||||
opacity: 75%;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.ff-inherit {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.fs-md {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
[align="center"],
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[align="right"],
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: var(--color-red) !important;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: var(--color-green) !important;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
align {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
align.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
align.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* lhs, rhs */
|
||||
.rhs {
|
||||
width: calc(100% - 23rem) !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.rhs {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
|
213
crates/app/src/public/css/utility.css
Normal file
213
crates/app/src/public/css/utility.css
Normal file
|
@ -0,0 +1,213 @@
|
|||
/* utility */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-rev-col {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.flex-rev-row {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-right {
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gap-1 {
|
||||
gap: var(--pad-1);
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: var(--pad-2);
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: var(--pad-4);
|
||||
}
|
||||
|
||||
.gap-8 {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.flex-collapse {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.sm\:static {
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
.mobile.flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.sm\:w-full {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.sm\:mt-2 {
|
||||
margin-top: 2rem !important;
|
||||
}
|
||||
|
||||
.sm\:items-start {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.sm\:contents {
|
||||
display: contents !important;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0 8px var(--color-shadow);
|
||||
}
|
||||
|
||||
.shadow-md {
|
||||
box-shadow: 0 8px 16px var(--color-shadow);
|
||||
}
|
||||
|
||||
.round-sm {
|
||||
border-radius: calc(var(--radius) / 2) !important;
|
||||
}
|
||||
|
||||
.round {
|
||||
border-radius: var(--radius) !important;
|
||||
}
|
||||
|
||||
.round-md {
|
||||
border-radius: calc(var(--radius) * 2) !important;
|
||||
}
|
||||
|
||||
.round-lg {
|
||||
border-radius: calc(var(--radius) * 4) !important;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.w-content {
|
||||
width: max-content !important;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[disabled="fully"] {
|
||||
opacity: 75%;
|
||||
pointer-events: visible;
|
||||
cursor: not-allowed;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fade,
|
||||
.CodeMirror-placeholder {
|
||||
opacity: 75%;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.ff-inherit {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.fs-md {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
[align="center"],
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[align="right"],
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: var(--color-red) !important;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: var(--color-green) !important;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
align {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
align.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
align.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* lhs, rhs */
|
||||
.rhs {
|
||||
width: calc(100% - 23rem) !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.rhs {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* turbo */
|
||||
.turbo-progress-bar {
|
||||
background: var(--color-primary);
|
||||
}
|
305
crates/app/src/public/html/body.lisp
Normal file
305
crates/app/src/public/html/body.lisp
Normal 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 %}")
|
|
@ -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 {
|
||||
|
|
|
@ -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 %}")
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);"))
|
||||
|
|
|
@ -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 %}")
|
||||
|
|
83
crates/app/src/public/html/forge/base.lisp
Normal file
83
crates/app/src/public/html/forge/base.lisp
Normal 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 %}")
|
80
crates/app/src/public/html/forge/home.lisp
Normal file
80
crates/app/src/public/html/forge/home.lisp
Normal 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 %}")
|
6
crates/app/src/public/html/forge/info.lisp
Normal file
6
crates/app/src/public/html/forge/info.lisp
Normal 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 %}")
|
|
@ -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 %}")
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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\" %}")))
|
||||
|
|
|
@ -29,7 +29,7 @@ use tetratto_core::{
|
|||
};
|
||||
|
||||
#[cfg(feature = "redis")]
|
||||
use redis::Commands;
|
||||
use tetratto_core::cache::redis::Commands;
|
||||
use tetratto_shared::hash;
|
||||
|
||||
pub async fn redirect_from_id(
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use std::{collections::HashMap, time::Duration};
|
||||
use redis::Commands;
|
||||
use axum::{
|
||||
extract::{
|
||||
ws::{Message as WsMessage, WebSocket, WebSocketUpgrade},
|
||||
|
@ -10,7 +9,7 @@ use axum::{
|
|||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::{
|
||||
cache::Cache,
|
||||
cache::{Cache, redis::Commands},
|
||||
model::{
|
||||
auth::User,
|
||||
channels::Message,
|
||||
|
|
|
@ -49,10 +49,15 @@ pub async fn create_request(
|
|||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
match data
|
||||
.create_community(Community::new(req.title, user.id))
|
||||
.await
|
||||
{
|
||||
let mut c = Community::new(req.title, user.id);
|
||||
|
||||
if req.forge {
|
||||
c.is_forge = true;
|
||||
c.context.enable_titles = true;
|
||||
c.context.require_titles = true;
|
||||
}
|
||||
|
||||
match data.create_community(c).await {
|
||||
Ok(id) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Community created".to_string(),
|
||||
|
@ -117,6 +122,16 @@ pub async fn update_context_request(
|
|||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
// check lengths
|
||||
if req.context.display_name.len() > 32 {
|
||||
return Json(Error::DataTooLong("display name".to_string()).into());
|
||||
}
|
||||
|
||||
if req.context.description.len() > 2_usize.pow(14) {
|
||||
return Json(Error::DataTooLong("description".to_string()).into());
|
||||
}
|
||||
|
||||
// ...
|
||||
match data.update_community_context(id, &user, req.context).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
|
|
|
@ -404,6 +404,8 @@ pub struct RegisterProps {
|
|||
#[derive(Deserialize)]
|
||||
pub struct CreateCommunity {
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub forge: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
|
@ -10,6 +10,8 @@ macro_rules! serve_asset {
|
|||
|
||||
serve_asset!(favicon_request: FAVICON("image/svg+xml"));
|
||||
serve_asset!(style_css_request: STYLE_CSS("text/css"));
|
||||
serve_asset!(root_css_request: ROOT_CSS("text/css"));
|
||||
serve_asset!(utility_css_request: UTILITY_CSS("text/css"));
|
||||
|
||||
serve_asset!(loader_js_request: LOADER_JS("text/javascript"));
|
||||
serve_asset!(atto_js_request: ATTO_JS("text/javascript"));
|
||||
|
|
|
@ -12,6 +12,8 @@ pub fn routes(config: &Config) -> Router {
|
|||
Router::new()
|
||||
// assets
|
||||
.route("/css/style.css", get(assets::style_css_request))
|
||||
.route("/css/root.css", get(assets::root_css_request))
|
||||
.route("/css/utility.css", get(assets::utility_css_request))
|
||||
.route("/js/loader.js", get(assets::loader_js_request))
|
||||
.route("/js/atto.js", get(assets::atto_js_request))
|
||||
.route("/js/me.js", get(assets::me_js_request))
|
||||
|
|
|
@ -11,14 +11,12 @@ use axum_extra::extract::CookieJar;
|
|||
use serde::Deserialize;
|
||||
use tera::Context;
|
||||
use tetratto_core::model::{
|
||||
auth::User,
|
||||
communities::{Community, CommunityReadAccess},
|
||||
communities_permissions::CommunityPermission,
|
||||
permissions::FinePermission,
|
||||
Error,
|
||||
auth::User, communities::Community, communities_permissions::CommunityPermission,
|
||||
permissions::FinePermission, Error,
|
||||
};
|
||||
|
||||
macro_rules! check_permissions {
|
||||
#[macro_export]
|
||||
macro_rules! check_community_permissions {
|
||||
($community:ident, $jar:ident, $data:ident, $user:ident) => {{
|
||||
let mut is_member: bool = false;
|
||||
let mut can_manage_pins: bool = false;
|
||||
|
@ -54,7 +52,7 @@ macro_rules! check_permissions {
|
|||
}
|
||||
|
||||
match $community.read_access {
|
||||
CommunityReadAccess::Joined => {
|
||||
tetratto_core::model::communities::CommunityReadAccess::Joined => {
|
||||
if !can_manage_communities {
|
||||
if !is_member {
|
||||
(false, can_manage_pins)
|
||||
|
@ -70,6 +68,7 @@ macro_rules! check_permissions {
|
|||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! community_context_bools {
|
||||
($data:ident, $user:ident, $community:ident) => {{
|
||||
let membership = if let Some(ref ua) = $user {
|
||||
|
@ -98,7 +97,9 @@ macro_rules! community_context_bools {
|
|||
};
|
||||
|
||||
let is_pending = if let Some(ref membership) = membership {
|
||||
membership.role.check(CommunityPermission::REQUESTED)
|
||||
membership.role.check(
|
||||
tetratto_core::model::communities_permissions::CommunityPermission::REQUESTED,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
@ -110,25 +111,27 @@ macro_rules! community_context_bools {
|
|||
};
|
||||
|
||||
let can_manage_posts = if let Some(ref membership) = membership {
|
||||
membership.role.check(CommunityPermission::MANAGE_POSTS)
|
||||
membership.role.check(
|
||||
tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_POSTS,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let can_manage_community = if let Some(ref membership) = membership {
|
||||
membership.role.check(CommunityPermission::MANAGE_COMMUNITY)
|
||||
membership.role.check(tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_COMMUNITY)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let can_manage_roles = if let Some(ref membership) = membership {
|
||||
membership.role.check(CommunityPermission::MANAGE_ROLES)
|
||||
membership.role.check(tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_ROLES)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let can_manage_questions = if let Some(ref membership) = membership {
|
||||
membership.role.check(CommunityPermission::MANAGE_QUESTIONS)
|
||||
membership.role.check(tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_QUESTIONS)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
@ -389,7 +392,7 @@ pub async fn feed_request(
|
|||
}
|
||||
|
||||
// check permissions
|
||||
let (can_read, _) = check_permissions!(community, jar, data, user);
|
||||
let (can_read, _) = check_community_permissions!(community, jar, data, user);
|
||||
|
||||
// ...
|
||||
let ignore_users = crate::ignore_users_gen!(user, data);
|
||||
|
@ -487,7 +490,7 @@ pub async fn questions_request(
|
|||
}
|
||||
|
||||
// check permissions
|
||||
let (can_read, _) = check_permissions!(community, jar, data, user);
|
||||
let (can_read, _) = check_community_permissions!(community, jar, data, user);
|
||||
|
||||
// ...
|
||||
let ignore_users = crate::ignore_users_gen!(user, data);
|
||||
|
@ -724,7 +727,7 @@ pub async fn post_request(
|
|||
};
|
||||
|
||||
// check permissions
|
||||
let (can_read, can_manage_pins) = check_permissions!(community, jar, data, user);
|
||||
let (can_read, can_manage_pins) = check_community_permissions!(community, jar, data, user);
|
||||
|
||||
if !can_read {
|
||||
return Err(Html(
|
||||
|
@ -838,7 +841,7 @@ pub async fn reposts_request(
|
|||
};
|
||||
|
||||
// check permissions
|
||||
let (can_read, _) = check_permissions!(community, jar, data, user);
|
||||
let (can_read, _) = check_community_permissions!(community, jar, data, user);
|
||||
|
||||
if !can_read {
|
||||
return Err(Html(
|
||||
|
@ -989,7 +992,7 @@ pub async fn likes_request(
|
|||
};
|
||||
|
||||
// check permissions
|
||||
let (can_read, _) = check_permissions!(community, jar, data, ua);
|
||||
let (can_read, _) = check_community_permissions!(community, jar, data, ua);
|
||||
|
||||
if !can_read {
|
||||
return Err(Html(
|
||||
|
@ -1092,7 +1095,7 @@ pub async fn members_request(
|
|||
}
|
||||
|
||||
// check permissions
|
||||
let (can_read, _) = check_permissions!(community, jar, data, user);
|
||||
let (can_read, _) = check_community_permissions!(community, jar, data, user);
|
||||
|
||||
// ...
|
||||
let list = match data
|
||||
|
@ -1128,6 +1131,7 @@ pub async fn members_request(
|
|||
can_manage_questions,
|
||||
) = community_context_bools!(data, user, community);
|
||||
|
||||
context.insert("allow_for_forges", &true);
|
||||
context.insert("list", &list);
|
||||
context.insert("page", &props.page);
|
||||
context.insert("owner", &owner);
|
||||
|
@ -1187,7 +1191,7 @@ pub async fn question_request(
|
|||
};
|
||||
|
||||
// check permissions
|
||||
let (can_read, _) = check_permissions!(community, jar, data, user);
|
||||
let (can_read, _) = check_community_permissions!(community, jar, data, user);
|
||||
|
||||
if !can_read && !is_sender {
|
||||
return Err(Html(
|
||||
|
|
126
crates/app/src/routes/pages/forge.rs
Normal file
126
crates/app/src/routes/pages/forge.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
use super::{communities::community_context, render_error};
|
||||
use crate::{
|
||||
assets::initial_context, check_community_permissions, community_context_bools, get_lang,
|
||||
get_user_from_token, State,
|
||||
};
|
||||
use axum::{
|
||||
response::{Html, IntoResponse},
|
||||
extract::Path,
|
||||
Extension,
|
||||
};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::{communities::Community, Error};
|
||||
|
||||
/// `/forges`
|
||||
pub async fn home_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
let data = data.read().await;
|
||||
let user = match get_user_from_token!(jar, data.0) {
|
||||
Some(ua) => ua,
|
||||
None => {
|
||||
return Err(Html(
|
||||
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let list = match data.0.get_memberships_by_owner(user.id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||
};
|
||||
|
||||
let mut communities: Vec<Community> = Vec::new();
|
||||
for membership in &list {
|
||||
match data
|
||||
.0
|
||||
.get_community_by_id_no_void(membership.community)
|
||||
.await
|
||||
{
|
||||
Ok(c) => {
|
||||
if c.is_forge {
|
||||
communities.push(c)
|
||||
} else {
|
||||
// we only want to show forges here
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// delete membership; community doesn't exist
|
||||
if let Err(e) = data.0.delete_membership(membership.id, &user).await {
|
||||
return Err(Html(render_error(e, &jar, &data, &Some(user)).await));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lang = get_lang!(jar, data.0);
|
||||
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||
context.insert("list", &communities);
|
||||
|
||||
// return
|
||||
Ok(Html(data.1.render("forge/home.html", &context).unwrap()))
|
||||
}
|
||||
|
||||
/// `/forge/{title}`
|
||||
pub async fn info_request(
|
||||
jar: CookieJar,
|
||||
Path(title): Path<String>,
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
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/info.html", &context).unwrap()))
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
pub mod auth;
|
||||
pub mod communities;
|
||||
pub mod forge;
|
||||
pub mod misc;
|
||||
pub mod mod_panel;
|
||||
pub mod profile;
|
||||
|
@ -110,6 +111,10 @@ pub fn routes() -> Router {
|
|||
"/chats/{community}/{channel}/_channels",
|
||||
get(chats::channels_request),
|
||||
)
|
||||
// forge
|
||||
.route("/forges", get(forge::home_request))
|
||||
.route("/forge/{title}", get(forge::info_request))
|
||||
.route("/forge/{title}/members", get(communities::members_request))
|
||||
// stacks
|
||||
.route("/stacks", get(stacks::list_request))
|
||||
.route("/stacks/{id}", get(stacks::posts_request))
|
||||
|
|
|
@ -12,12 +12,12 @@ default = ["sqlite", "redis"]
|
|||
[dependencies]
|
||||
pathbufd = "0.1.4"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
toml = "0.8.22"
|
||||
toml = "0.8.23"
|
||||
tetratto-shared = { path = "../shared" }
|
||||
tetratto-l10n = { path = "../l10n" }
|
||||
serde_json = "1.0.140"
|
||||
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] }
|
||||
reqwest = { version = "0.12.18", features = ["json"] }
|
||||
reqwest = { version = "0.12.19", features = ["json"] }
|
||||
bitflags = "2.9.1"
|
||||
async-recursion = "1.1.1"
|
||||
md-5 = "0.10.6"
|
||||
|
@ -25,4 +25,4 @@ base16ct = { version = "0.2.0", features = ["alloc"] }
|
|||
base64 = "0.22.1"
|
||||
emojis = "0.6.4"
|
||||
regex = "1.11.1"
|
||||
oiseau = { version = "0.1.0", default-features = false }
|
||||
oiseau = { version = "0.1.2", default-features = false }
|
||||
|
|
|
@ -10,7 +10,10 @@ use crate::model::{
|
|||
};
|
||||
use pathbufd::PathBufD;
|
||||
use std::fs::{exists, remove_file};
|
||||
use tetratto_shared::hash::{hash_salted, salt};
|
||||
use tetratto_shared::{
|
||||
hash::{hash_salted, salt},
|
||||
unix_epoch_timestamp,
|
||||
};
|
||||
use crate::{auto_method, DataManager};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
|
@ -18,8 +21,6 @@ use oiseau::SqliteRow;
|
|||
|
||||
#[cfg(feature = "postgres")]
|
||||
use oiseau::PostgresRow;
|
||||
#[cfg(feature = "postgres")]
|
||||
use tetratto_shared::unix_epoch_timestamp;
|
||||
|
||||
use oiseau::{execute, get, query_row, params};
|
||||
|
||||
|
|
|
@ -40,8 +40,10 @@ impl DataManager {
|
|||
// likes
|
||||
likes: get!(x->8(i32)) as isize,
|
||||
dislikes: get!(x->9(i32)) as isize,
|
||||
// counts
|
||||
// ...
|
||||
member_count: get!(x->10(i32)) as usize,
|
||||
is_forge: get!(x->11(i32)) as i8 == 1,
|
||||
post_count: get!(x->12(i32)) as usize,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,7 +53,10 @@ impl DataManager {
|
|||
}
|
||||
|
||||
if let Some(cached) = self.0.1.get(format!("atto.community:{}", id)).await {
|
||||
return Ok(serde_json::from_str(&cached).unwrap());
|
||||
match serde_json::from_str(&cached) {
|
||||
Ok(c) => return Ok(c),
|
||||
Err(_) => self.0.1.remove(format!("atto.community:{}", id)).await,
|
||||
};
|
||||
}
|
||||
|
||||
let conn = match self.0.connect().await {
|
||||
|
@ -89,7 +94,10 @@ impl DataManager {
|
|||
}
|
||||
|
||||
if let Some(cached) = self.0.1.get(format!("atto.community:{}", id)).await {
|
||||
return Ok(serde_json::from_str(&cached).unwrap());
|
||||
match serde_json::from_str(&cached) {
|
||||
Ok(c) => return Ok(c),
|
||||
Err(_) => self.0.1.remove(format!("atto.community:{}", id)).await,
|
||||
};
|
||||
}
|
||||
|
||||
let conn = match self.0.connect().await {
|
||||
|
@ -218,7 +226,7 @@ impl DataManager {
|
|||
return Err(Error::MiscError("This title cannot be used".to_string()));
|
||||
}
|
||||
|
||||
let regex = regex::RegexBuilder::new(r"[^\w_\-\.!]+")
|
||||
let regex = regex::RegexBuilder::new(NAME_REGEX)
|
||||
.multi_line(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -259,6 +267,12 @@ impl DataManager {
|
|||
}
|
||||
}
|
||||
|
||||
// check is_forge
|
||||
// only supporters can CREATE forge communities... anybody can contribute to them
|
||||
if data.is_forge && !owner.permissions.check(FinePermission::SUPPORTER) {
|
||||
return Err(Error::RequiresSupporter);
|
||||
}
|
||||
|
||||
// make sure community doesn't already exist with title
|
||||
if self
|
||||
.get_community_by_title_no_void(&data.title.to_lowercase())
|
||||
|
@ -276,7 +290,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO communities VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
||||
"INSERT INTO communities VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.created as i64),
|
||||
|
@ -288,7 +302,9 @@ impl DataManager {
|
|||
&serde_json::to_string(&data.join_access).unwrap().as_str(),
|
||||
&0_i32,
|
||||
&0_i32,
|
||||
&1_i32
|
||||
&1_i32,
|
||||
&{ if data.is_forge { 1 } else { 0 } },
|
||||
&0_i32,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -532,4 +548,7 @@ impl DataManager {
|
|||
|
||||
auto_method!(incr_community_member_count()@get_community_by_id_no_void -> "UPDATE communities SET member_count = member_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr);
|
||||
auto_method!(decr_community_member_count()@get_community_by_id_no_void -> "UPDATE communities SET member_count = member_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr=member_count);
|
||||
|
||||
auto_method!(incr_community_post_count()@get_community_by_id_no_void -> "UPDATE communities SET post_count = post_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr);
|
||||
auto_method!(decr_community_post_count()@get_community_by_id_no_void -> "UPDATE communities SET post_count = post_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr=post_count);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use crate::model::{
|
|||
communities_permissions::CommunityPermission, channels::Message,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use tetratto_shared::unix_epoch_timestamp;
|
||||
use crate::{auto_method, DataManager};
|
||||
|
||||
#[cfg(feature = "redis")]
|
||||
|
@ -18,8 +19,6 @@ use oiseau::SqliteRow;
|
|||
|
||||
#[cfg(feature = "postgres")]
|
||||
use oiseau::PostgresRow;
|
||||
#[cfg(feature = "postgres")]
|
||||
use tetratto_shared::unix_epoch_timestamp;
|
||||
|
||||
use oiseau::{execute, get, query_rows, params};
|
||||
|
||||
|
|
|
@ -872,7 +872,7 @@ impl DataManager {
|
|||
|
||||
let res = query_row!(
|
||||
&conn,
|
||||
"SELECT * FROM posts WHERE context LIKE $1 AND owner = $2 LIMIT 1",
|
||||
"SELECT * FROM posts WHERE context LIKE $1 AND owner = $2 AND is_deleted = 0 LIMIT 1",
|
||||
params![&format!("%\"answering\":{question}%"), &(owner as i64),],
|
||||
|x| { Ok(Self::get_post_from_row(x)) }
|
||||
);
|
||||
|
@ -1494,6 +1494,9 @@ impl DataManager {
|
|||
// increase user post count
|
||||
self.incr_user_post_count(data.owner).await?;
|
||||
|
||||
// increase community post count
|
||||
self.incr_community_post_count(data.community).await?;
|
||||
|
||||
// return
|
||||
Ok(data.id)
|
||||
}
|
||||
|
@ -1546,6 +1549,13 @@ impl DataManager {
|
|||
self.decr_user_post_count(y.owner).await?;
|
||||
}
|
||||
|
||||
// decr community post count
|
||||
let community = self.get_community_by_id_no_void(y.community).await?;
|
||||
|
||||
if community.post_count > 0 {
|
||||
self.decr_community_post_count(y.community).await?;
|
||||
}
|
||||
|
||||
// decr question answer count
|
||||
if y.context.answering != 0 {
|
||||
let question = self.get_question_by_id(y.context.answering).await?;
|
||||
|
@ -1622,12 +1632,19 @@ impl DataManager {
|
|||
self.decr_user_post_count(y.owner).await?;
|
||||
}
|
||||
|
||||
// decr community post count
|
||||
let community = self.get_community_by_id_no_void(y.community).await?;
|
||||
|
||||
if community.post_count > 0 {
|
||||
self.decr_community_post_count(y.community).await?;
|
||||
}
|
||||
|
||||
// decr question answer count
|
||||
if y.context.answering != 0 {
|
||||
let question = self.get_question_by_id(y.context.answering).await?;
|
||||
|
||||
if question.is_global {
|
||||
self.incr_question_answer_count(y.context.answering).await?;
|
||||
self.decr_question_answer_count(y.context.answering).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1644,12 +1661,15 @@ impl DataManager {
|
|||
// incr user post count
|
||||
self.incr_user_post_count(y.owner).await?;
|
||||
|
||||
// incr community post count
|
||||
self.incr_community_post_count(y.community).await?;
|
||||
|
||||
// incr question answer count
|
||||
if y.context.answering != 0 {
|
||||
let question = self.get_question_by_id(y.context.answering).await?;
|
||||
|
||||
if question.is_global {
|
||||
self.decr_question_answer_count(y.context.answering).await?;
|
||||
self.incr_question_answer_count(y.context.answering).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,8 +22,10 @@ pub struct Community {
|
|||
// likes
|
||||
pub likes: isize,
|
||||
pub dislikes: isize,
|
||||
// counts
|
||||
// ...
|
||||
pub member_count: usize,
|
||||
pub is_forge: bool,
|
||||
pub post_count: usize,
|
||||
}
|
||||
|
||||
impl Community {
|
||||
|
@ -44,6 +46,8 @@ impl Community {
|
|||
likes: 0,
|
||||
dislikes: 0,
|
||||
member_count: 0,
|
||||
is_forge: false,
|
||||
post_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,6 +66,8 @@ impl Community {
|
|||
likes: 0,
|
||||
dislikes: 0,
|
||||
member_count: 0,
|
||||
is_forge: false,
|
||||
post_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,9 +90,6 @@ pub struct CommunityContext {
|
|||
/// `enable_titles` is required for this setting to work.
|
||||
#[serde(default)]
|
||||
pub require_titles: bool,
|
||||
/// The community's layout in the UI.
|
||||
#[serde(default)]
|
||||
pub layout: CommunityLayout,
|
||||
}
|
||||
|
||||
/// Who can read a [`Community`].
|
||||
|
@ -140,21 +143,6 @@ impl Default for CommunityJoinAccess {
|
|||
}
|
||||
}
|
||||
|
||||
/// The layout of the [`Community`]'s UI.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum CommunityLayout {
|
||||
/// The classic timeline-like layout.
|
||||
Classic,
|
||||
/// A GitHub-esque bug tracker layout.
|
||||
BugTracker,
|
||||
}
|
||||
|
||||
impl Default for CommunityLayout {
|
||||
fn default() -> Self {
|
||||
Self::Classic
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommunityMembership {
|
||||
pub id: usize,
|
||||
|
|
|
@ -9,4 +9,4 @@ license.workspace = true
|
|||
[dependencies]
|
||||
pathbufd = "0.1.4"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
toml = "0.8.22"
|
||||
toml = "0.8.23"
|
||||
|
|
2
sql_changes/communities_is_forge.sql
Normal file
2
sql_changes/communities_is_forge.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE communities
|
||||
ADD COLUMN is_forge INT NOT NULL DEFAULT 0;
|
2
sql_changes/communities_post_count.sql
Normal file
2
sql_changes/communities_post_count.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE communities
|
||||
ADD COLUMN post_count INT NOT NULL DEFAULT 0;
|
Loading…
Add table
Add a link
Reference in a new issue