add: better user settings page
This commit is contained in:
parent
e8cc541f45
commit
4735832cef
16 changed files with 2398 additions and 2241 deletions
|
@ -25,6 +25,10 @@ cd ../tetratto
|
||||||
|
|
||||||
Your first start of Tetratto might be a little slow as it's going to download all icon SVGs required for the HTML templates to render properly. These icons will be stored on disk, so there's no need to worry about this time _every_ restart.
|
Your first start of Tetratto might be a little slow as it's going to download all icon SVGs required for the HTML templates to render properly. These icons will be stored on disk, so there's no need to worry about this time _every_ restart.
|
||||||
|
|
||||||
|
Tetratto attempts to load `/public/fonts/lexend_variable.woff2` by defualt. This font is not included in the source by default, so you must download it yourself. Download Lexend (available from Google Fonts) as a variable font, and then create a `fonts` directory in the created `public` directory (relative to your configuration file). Place the font file (named "lexend_variable.woff2") in this fonts directory.
|
||||||
|
|
||||||
|
Please note that Google Fonts only distributes Lexend Variable as a TTF file. You can use [`woff2_convert`](https://github.com/google/woff2) to convert the TTF into a woff2 file (`woff2_convert lexend_variable.ttf`).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
In the directory you're running Tetratto from, you should create a `tetratto.toml` file. This file follows the configuration schema defined [here](https://trisuaso.github.io/tetratto/tetratto/config/struct.Config.html)!
|
In the directory you're running Tetratto from, you should create a `tetratto.toml` file. This file follows the configuration schema defined [here](https://trisuaso.github.io/tetratto/tetratto/config/struct.Config.html)!
|
||||||
|
|
|
@ -80,6 +80,7 @@ version = "1.0.0"
|
||||||
"auth:label.relationship" = "Relationship"
|
"auth:label.relationship" = "Relationship"
|
||||||
"auth:label.joined_communities" = "Joined communities"
|
"auth:label.joined_communities" = "Joined communities"
|
||||||
"auth:label.recent_posts" = "Recent posts"
|
"auth:label.recent_posts" = "Recent posts"
|
||||||
|
"auth:label.recent_answers" = "Recent answers"
|
||||||
"auth:label.recent_with_tag" = "Recent posts (with tag)"
|
"auth:label.recent_with_tag" = "Recent posts (with tag)"
|
||||||
"auth:label.recent_replies" = "Recent replies"
|
"auth:label.recent_replies" = "Recent replies"
|
||||||
"auth:label.recent_posts_with_media" = "Recent posts (with media)"
|
"auth:label.recent_posts_with_media" = "Recent posts (with media)"
|
||||||
|
@ -176,7 +177,7 @@ version = "1.0.0"
|
||||||
"settings:tab.profile" = "Profile"
|
"settings:tab.profile" = "Profile"
|
||||||
"settings:tab.theme" = "Theme"
|
"settings:tab.theme" = "Theme"
|
||||||
"settings:tab.sessions" = "Sessions"
|
"settings:tab.sessions" = "Sessions"
|
||||||
"settings:tab.connections" = "Connections"
|
"settings:tab.grants" = "Grants"
|
||||||
"settings:tab.images" = "Images"
|
"settings:tab.images" = "Images"
|
||||||
"settings:tab.presets" = "Presets"
|
"settings:tab.presets" = "Presets"
|
||||||
"settings:label.change_password" = "Change password"
|
"settings:label.change_password" = "Change password"
|
||||||
|
|
|
@ -85,13 +85,18 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Lexend";
|
||||||
|
src: url("/public/fonts/lexend_variable.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
letter-spacing: 0.15px;
|
letter-spacing: 0.15px;
|
||||||
font-family:
|
font-family:
|
||||||
"Inter", "Poppins", "Roboto", ui-sans-serif, system-ui, sans-serif,
|
"Lexend", "Inter", "Poppins", "Roboto", ui-sans-serif, system-ui,
|
||||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||||
"Noto Color Emoji";
|
"Noto Color Emoji";
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
|
|
|
@ -466,7 +466,7 @@ button.camo:hover,
|
||||||
input,
|
input,
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
padding: 0.35rem var(--pad-3);
|
padding: var(--pad-2) var(--pad-3);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
|
@ -481,6 +481,10 @@ select {
|
||||||
color: var(--color-text-lowered);
|
color: var(--color-text-lowered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
min-height: 5rem;
|
min-height: 5rem;
|
||||||
}
|
}
|
||||||
|
@ -564,10 +568,27 @@ input[type="checkbox"]:checked {
|
||||||
background-image: url("/icons/check.svg");
|
background-image: url("/icons/check.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
height: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.round_form input,
|
||||||
|
.round_form .square {
|
||||||
|
border-radius: var(--circle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.round_form input {
|
||||||
|
padding: var(--pad-2) var(--pad-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.round_form input[type="file"] {
|
||||||
|
padding: 0 var(--pad-4);
|
||||||
|
}
|
||||||
|
|
||||||
/* pillmenu */
|
/* pillmenu */
|
||||||
.pillmenu {
|
.pillmenu {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -874,12 +895,12 @@ nav .button:not(.title):not(.active):hover {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile_nav .pillmenu a:first-of-type {
|
.mobile_nav .pillmenu a:not(.dropdown *):first-of-type {
|
||||||
border-top-left-radius: var(--radius) !important;
|
border-top-left-radius: var(--radius) !important;
|
||||||
border-bottom-left-radius: var(--radius) !important;
|
border-bottom-left-radius: var(--radius) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile_nav .pillmenu a:last-of-type {
|
.mobile_nav .pillmenu a:not(.dropdown *):last-of-type {
|
||||||
border-top-right-radius: var(--radius) !important;
|
border-top-right-radius: var(--radius) !important;
|
||||||
border-bottom-right-radius: var(--radius) !important;
|
border-bottom-right-radius: var(--radius) !important;
|
||||||
}
|
}
|
||||||
|
@ -1033,14 +1054,14 @@ dialog:is(.dark *)::backdrop {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown .inner .active::after {
|
.dropdown .inner .active::after {
|
||||||
top: 0;
|
top: 10%;
|
||||||
left: 0;
|
left: 5px;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
content: "";
|
content: "";
|
||||||
height: 100%;
|
height: 80%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--circle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown:not(nav *):has(.inner.open) button:not(.inner button) {
|
.dropdown:not(nav *):has(.inner.open) button:not(.inner button) {
|
||||||
|
@ -1085,7 +1106,7 @@ dialog:is(.dark *)::backdrop {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
max-width: calc(100dvw - var(--pad-4));
|
max-width: calc(100dvw - var(--pad-4));
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: var(--pad-3) var(--pad-4);
|
padding: var(--pad-2) var(--pad-3);
|
||||||
animation: popin ease-in-out 1 0.15s running;
|
animation: popin ease-in-out 1 0.15s running;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -1502,3 +1523,47 @@ details.accordion .inner {
|
||||||
top: 0;
|
top: 0;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* menus */
|
||||||
|
menu {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu a {
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
text-decoration: none !important;
|
||||||
|
background: var(--color-raised);
|
||||||
|
color: var(--color-text-raised);
|
||||||
|
padding: var(--pad-2) var(--pad-3);
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--pad-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu a:hover {
|
||||||
|
background: var(--color-super-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu a.active {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.col {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 25rem;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: var(--pad-3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu a:first-child {
|
||||||
|
border-top-left-radius: var(--radius);
|
||||||
|
border-top-right-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu a:last-child {
|
||||||
|
border-bottom-left-radius: var(--radius);
|
||||||
|
border-bottom-right-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
`<b>${message}.</b> You can now close this tab.`;
|
`<b>${message}.</b> You can now close this tab.`;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = \"/settings#/connections\";
|
window.location.href = \"/settings#/grants\";
|
||||||
}, 500);
|
}, 500);
|
||||||
}, 1000);"))
|
}, 1000);"))
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
`<b>${message}.</b> You can now close this tab.`;
|
`<b>${message}.</b> You can now close this tab.`;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = \"/settings#/connections\";
|
window.location.href = \"/settings#/grants\";
|
||||||
}, 500);
|
}, 500);
|
||||||
}, 1000);"))
|
}, 1000);"))
|
||||||
|
|
||||||
|
|
|
@ -188,7 +188,7 @@
|
||||||
(b
|
(b
|
||||||
(text "{{ text \"settings:label.change_avatar\" }}")))
|
(text "{{ text \"settings:label.change_avatar\" }}")))
|
||||||
(form
|
(form
|
||||||
("class" "card flex gap_2 flex_row flex_wrap items_center")
|
("class" "card big_icon flex gap_2 flex_row flex_wrap items_center")
|
||||||
("method" "post")
|
("method" "post")
|
||||||
("enctype" "multipart/form-data")
|
("enctype" "multipart/form-data")
|
||||||
("onsubmit" "upload_avatar(event)")
|
("onsubmit" "upload_avatar(event)")
|
||||||
|
@ -199,6 +199,7 @@
|
||||||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||||
("class" "w_content"))
|
("class" "w_content"))
|
||||||
(button
|
(button
|
||||||
|
("class" "small square big_icon")
|
||||||
(text "{{ icon \"check\" }}"))))
|
(text "{{ icon \"check\" }}"))))
|
||||||
(div
|
(div
|
||||||
("class" "card_nest")
|
("class" "card_nest")
|
||||||
|
@ -208,7 +209,7 @@
|
||||||
(b
|
(b
|
||||||
(text "{{ text \"settings:label.change_banner\" }}")))
|
(text "{{ text \"settings:label.change_banner\" }}")))
|
||||||
(form
|
(form
|
||||||
("class" "card flex flex_col gap_2")
|
("class" "card big_icon flex flex_col gap_2")
|
||||||
("method" "post")
|
("method" "post")
|
||||||
("enctype" "multipart/form-data")
|
("enctype" "multipart/form-data")
|
||||||
("onsubmit" "upload_banner(event)")
|
("onsubmit" "upload_banner(event)")
|
||||||
|
@ -221,6 +222,7 @@
|
||||||
("accept" "image/png,image/jpeg,image/avif,image/webp")
|
("accept" "image/png,image/jpeg,image/avif,image/webp")
|
||||||
("class" "w_content"))
|
("class" "w_content"))
|
||||||
(button
|
(button
|
||||||
|
("class" "small square big_icon")
|
||||||
(text "{{ icon \"check\" }}")))
|
(text "{{ icon \"check\" }}")))
|
||||||
(span
|
(span
|
||||||
("class" "fade")
|
("class" "fade")
|
||||||
|
|
|
@ -18,17 +18,7 @@
|
||||||
(span
|
(span
|
||||||
("class" "desktop")
|
("class" "desktop")
|
||||||
(str (text "general:link.home"))))
|
(str (text "general:link.home"))))
|
||||||
|
(text "{%- endif %}"))
|
||||||
(text "{% if user -%}")
|
|
||||||
(a
|
|
||||||
("href" "/communities")
|
|
||||||
("class" "button {% if selected == 'communities' -%}active{%- endif %}")
|
|
||||||
(icon (text "book-heart"))
|
|
||||||
(span
|
|
||||||
("class" "desktop")
|
|
||||||
(str (text "general:link.communities"))))
|
|
||||||
|
|
||||||
(text "{%- endif %} {%- endif %}"))
|
|
||||||
|
|
||||||
(div
|
(div
|
||||||
("class" "flex nav_side")
|
("class" "flex nav_side")
|
||||||
|
@ -71,6 +61,10 @@
|
||||||
|
|
||||||
(div
|
(div
|
||||||
("class" "inner")
|
("class" "inner")
|
||||||
|
(a
|
||||||
|
("href" "/communities")
|
||||||
|
(icon (text "book-heart"))
|
||||||
|
(str (text "general:link.communities")))
|
||||||
(a
|
(a
|
||||||
("href" "/chats/0/0")
|
("href" "/chats/0/0")
|
||||||
(icon (text "message-circle"))
|
(icon (text "message-circle"))
|
||||||
|
@ -385,9 +379,9 @@
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"settings:tab.sessions\" }}")))
|
(text "{{ text \"settings:tab.sessions\" }}")))
|
||||||
(a
|
(a
|
||||||
("data-tab-button" "connections")
|
("data-tab-button" "grants")
|
||||||
("href" "#/connections")
|
("href" "#/grants")
|
||||||
(text "{{ icon \"cable\" }}")
|
(text "{{ icon \"cable\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"settings:tab.connections\" }}")))
|
(text "{{ text \"settings:tab.grants\" }}")))
|
||||||
(text "{%- endmacro %}")
|
(text "{%- endmacro %}")
|
||||||
|
|
|
@ -20,6 +20,28 @@
|
||||||
(b
|
(b
|
||||||
(text "{{ tag }}")))
|
(text "{{ tag }}")))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
|
(text "{% if not tag -%}")
|
||||||
|
(div
|
||||||
|
("class" "flex gap_2")
|
||||||
|
(div
|
||||||
|
("class" "dropdown")
|
||||||
|
(button
|
||||||
|
("class" "lowered small")
|
||||||
|
("onclick" "trigger('atto::hooks::dropdown', [event])")
|
||||||
|
("exclude" "dropdown")
|
||||||
|
(icon (text "arrow-down-up"))
|
||||||
|
(text "{{ order }}"))
|
||||||
|
(div
|
||||||
|
("class" "inner")
|
||||||
|
(a
|
||||||
|
("href" "?o=Recent&f=true")
|
||||||
|
(icon (text "calendar-arrow-down"))
|
||||||
|
(span (text "Recent")))
|
||||||
|
(a
|
||||||
|
("href" "?o=Popular&f=true")
|
||||||
|
(icon (text "trending-up"))
|
||||||
|
(span (text "Popular")))))
|
||||||
|
|
||||||
(text "{% if user -%}")
|
(text "{% if user -%}")
|
||||||
(a
|
(a
|
||||||
("href" "/search?profile={{ profile.id }}")
|
("href" "/search?profile={{ profile.id }}")
|
||||||
|
@ -28,6 +50,7 @@
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:link.search\" }}")))
|
(text "{{ text \"general:link.search\" }}")))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
|
(text "{%- endif %}"))
|
||||||
(div
|
(div
|
||||||
("class" "card w_full flex flex_col gap_2")
|
("class" "card w_full flex flex_col gap_2")
|
||||||
("ui_ident" "io_data_load")
|
("ui_ident" "io_data_load")
|
||||||
|
@ -42,7 +65,7 @@
|
||||||
(text "{% set paged = user and user.settings.paged_timelines %}")
|
(text "{% set paged = user and user.settings.paged_timelines %}")
|
||||||
(script
|
(script
|
||||||
(text "setTimeout(async () => {
|
(text "setTimeout(async () => {
|
||||||
await trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?user_id={{ profile.id }}&tag={{ tag }}&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
|
await trigger(\"ui::io_data_load\", [\"/_swiss_army_timeline?user_id={{ profile.id }}&tag={{ tag }}&order={{ order }}&page=\", Number.parseInt(\"{{ page }}\") - 1, \"{{ paged }}\" === \"true\"]);
|
||||||
(await ns(\"ui\")).IO_DATA_DISABLE_RELOAD = true;
|
(await ns(\"ui\")).IO_DATA_DISABLE_RELOAD = true;
|
||||||
console.log(\"created profile timeline\");
|
console.log(\"created profile timeline\");
|
||||||
}, 1000);"))
|
}, 1000);"))
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
("class" "flex gap_2 items_center")
|
("class" "flex gap_2 items_center")
|
||||||
(text "{% if not tag -%} {{ icon \"clock\" }}")
|
(text "{% if not tag -%} {{ icon \"clock\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"auth:label.recent_posts\" }}"))
|
(text "{{ text \"auth:label.recent_answers\" }}"))
|
||||||
(text "{% else %} {{ icon \"tag\" }}")
|
(text "{% else %} {{ icon \"tag\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"auth:label.recent_with_tag\" }}: ")
|
(text "{{ text \"auth:label.recent_with_tag\" }}: ")
|
||||||
|
|
|
@ -2,8 +2,16 @@
|
||||||
(title
|
(title
|
||||||
(text "Settings - {{ config.name }}"))
|
(text "Settings - {{ config.name }}"))
|
||||||
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
|
||||||
(main
|
(article
|
||||||
("class" "flex flex_col gap_2")
|
("class" "flex flex_row gap_2 content_container")
|
||||||
|
; nav desktop
|
||||||
|
(menu
|
||||||
|
("class" "desktop col")
|
||||||
|
(text "{{ macros::profile_settings_nav_options() }}"))
|
||||||
|
|
||||||
|
; content
|
||||||
|
(main
|
||||||
|
("class" "flex flex_col gap_2 w_full")
|
||||||
(text "{% if profile.id != user.id -%}")
|
(text "{% if profile.id != user.id -%}")
|
||||||
(div
|
(div
|
||||||
("class" "card w_full red flex gap_2 items_center")
|
("class" "card w_full red flex gap_2 items_center")
|
||||||
|
@ -29,11 +37,6 @@
|
||||||
("class" "inner left")
|
("class" "inner left")
|
||||||
(text "{{ macros::profile_settings_nav_options() }}"))))
|
(text "{{ macros::profile_settings_nav_options() }}"))))
|
||||||
|
|
||||||
; nav desktop
|
|
||||||
(div
|
|
||||||
("class" "desktop pillmenu")
|
|
||||||
(text "{{ macros::profile_settings_nav_options() }}"))
|
|
||||||
|
|
||||||
; ...
|
; ...
|
||||||
(div
|
(div
|
||||||
("class" "w_full flex flex_col gap_2 hidden")
|
("class" "w_full flex flex_col gap_2 hidden")
|
||||||
|
@ -125,6 +128,7 @@
|
||||||
(div
|
(div
|
||||||
("class" "pillmenu")
|
("class" "pillmenu")
|
||||||
("ui_ident" "account_settings_tabs")
|
("ui_ident" "account_settings_tabs")
|
||||||
|
("style" "z-index: 0")
|
||||||
(a
|
(a
|
||||||
("data-tab-button" "account/security")
|
("data-tab-button" "account/security")
|
||||||
("href" "#/account/security")
|
("href" "#/account/security")
|
||||||
|
@ -941,7 +945,7 @@
|
||||||
(b
|
(b
|
||||||
(text "{{ text \"settings:label.change_avatar\" }}")))
|
(text "{{ text \"settings:label.change_avatar\" }}")))
|
||||||
(form
|
(form
|
||||||
("class" "card flex gap_2 flex_row flex_wrap items_center")
|
("class" "card round_form flex gap_2 flex_row flex_wrap items_center")
|
||||||
("method" "post")
|
("method" "post")
|
||||||
("enctype" "multipart/form-data")
|
("enctype" "multipart/form-data")
|
||||||
("onsubmit" "upload_avatar(event)")
|
("onsubmit" "upload_avatar(event)")
|
||||||
|
@ -954,6 +958,7 @@
|
||||||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||||
("class" "w_content"))
|
("class" "w_content"))
|
||||||
(button
|
(button
|
||||||
|
("class" "small square big_icon")
|
||||||
(text "{{ icon \"check\" }}")))
|
(text "{{ icon \"check\" }}")))
|
||||||
(span
|
(span
|
||||||
("class" "fade")
|
("class" "fade")
|
||||||
|
@ -968,7 +973,7 @@
|
||||||
(b
|
(b
|
||||||
(text "{{ text \"settings:label.change_banner\" }}")))
|
(text "{{ text \"settings:label.change_banner\" }}")))
|
||||||
(form
|
(form
|
||||||
("class" "card flex flex_col gap_2")
|
("class" "card round_form flex flex_col gap_2")
|
||||||
("method" "post")
|
("method" "post")
|
||||||
("enctype" "multipart/form-data")
|
("enctype" "multipart/form-data")
|
||||||
("onsubmit" "upload_banner(event)")
|
("onsubmit" "upload_banner(event)")
|
||||||
|
@ -981,6 +986,7 @@
|
||||||
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
|
||||||
("class" "w_content"))
|
("class" "w_content"))
|
||||||
(button
|
(button
|
||||||
|
("class" "small square big_icon")
|
||||||
(text "{{ icon \"check\" }}")))
|
(text "{{ icon \"check\" }}")))
|
||||||
(span
|
(span
|
||||||
("class" "fade")
|
("class" "fade")
|
||||||
|
@ -1190,7 +1196,7 @@
|
||||||
(text "{{ text \"general:action.save\" }}"))))
|
(text "{{ text \"general:action.save\" }}"))))
|
||||||
(div
|
(div
|
||||||
("class" "card w_full lowered hidden flex flex_col gap_2")
|
("class" "card w_full lowered hidden flex flex_col gap_2")
|
||||||
("data-tab" "connections")
|
("data-tab" "grants")
|
||||||
(div
|
(div
|
||||||
("class" "card w_full flex flex_wrap gap_2")
|
("class" "card w_full flex flex_wrap gap_2")
|
||||||
(text "{% if config.connections.spotify_client_id and not profile.connections.Spotify %}")
|
(text "{% if config.connections.spotify_client_id and not profile.connections.Spotify %}")
|
||||||
|
@ -2298,6 +2304,5 @@
|
||||||
anchor.click();
|
anchor.click();
|
||||||
anchor.remove();
|
anchor.remove();
|
||||||
};
|
};
|
||||||
});")))
|
});"))))
|
||||||
|
|
||||||
(text "{% endblock %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
("class" "card w_full flex flex_col gap_2")
|
("class" "card w_full flex flex_col gap_2")
|
||||||
(text "{% if not profile and not user.permissions|has_supporter -%} {{ components::supporter_ad(body=\"Become a supporter for full-site search!\") }} {% else %}")
|
(text "{% if not profile and not user.permissions|has_supporter -%} {{ components::supporter_ad(body=\"Become a supporter for full-site search!\") }} {% else %}")
|
||||||
(form
|
(form
|
||||||
("class" "flex flex_col gap_2")
|
("class" "flex flex_col gap_2 round_form")
|
||||||
(div
|
(div
|
||||||
("class" "flex flex_row gap_2")
|
("class" "flex flex_row gap_2")
|
||||||
(input
|
(input
|
||||||
|
@ -57,5 +57,4 @@
|
||||||
(text "{%- endif %}"))))
|
(text "{%- endif %}"))))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(text "{% for post in list %} {% if post[2].read_access == \"Everybody\" -%} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2], poll=post[5]) }} {%- endif %} {%- endif %} {% endfor %} {% if profile -%} {{ components::pagination(page=page, items=list|length, key=\"&profile=\" ~ profile.id, value=\"&query=\" ~ query) }} {% else %} {{ components::pagination(page=page, items=list|length, key=\"&query=\" ~ query) }} {%- endif %}"))))
|
(text "{% for post in list %} {% if post[2].read_access == \"Everybody\" -%} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2], poll=post[5]) }} {%- endif %} {%- endif %} {% endfor %} {% if profile -%} {{ components::pagination(page=page, items=list|length, key=\"&profile=\" ~ profile.id, value=\"&query=\" ~ query) }} {% else %} {{ components::pagination(page=page, items=list|length, key=\"&query=\" ~ query) }} {%- endif %}"))))
|
||||||
|
|
||||||
(text "{% endblock %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -217,7 +217,14 @@ pub async fn add_user_request(
|
||||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// get stack
|
||||||
|
let mut stack = match data.get_stack_by_id(id).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
// check block status
|
// check block status
|
||||||
|
if stack.mode != StackMode::BlockList {
|
||||||
if data
|
if data
|
||||||
.get_userblock_by_initiator_receiver(other_user.id, user.id)
|
.get_userblock_by_initiator_receiver(other_user.id, user.id)
|
||||||
.await
|
.await
|
||||||
|
@ -225,13 +232,9 @@ pub async fn add_user_request(
|
||||||
{
|
{
|
||||||
return Json(Error::NotAllowed.into());
|
return Json(Error::NotAllowed.into());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// add user
|
// add user
|
||||||
let mut stack = match data.get_stack_by_id(id).await {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => return Json(e.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if stack.users.contains(&other_user.id) {
|
if stack.users.contains(&other_user.id) {
|
||||||
return Json(Error::MiscError("This user is already in this stack".to_string()).into());
|
return Json(Error::MiscError("This user is already in this stack".to_string()).into());
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use axum::{
|
||||||
Extension,
|
Extension,
|
||||||
};
|
};
|
||||||
use crate::cookie::CookieJar;
|
use crate::cookie::CookieJar;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use tetratto_core::{
|
use tetratto_core::{
|
||||||
database::FullPost,
|
database::FullPost,
|
||||||
model::{
|
model::{
|
||||||
|
@ -670,6 +670,20 @@ pub async fn search_request(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
|
||||||
|
pub enum TimelineOrderMode {
|
||||||
|
/// Ordered by creation date.
|
||||||
|
Recent,
|
||||||
|
/// Ordered by likes - dislikes.
|
||||||
|
Popular,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TimelineOrderMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Recent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct TimelineQuery {
|
pub struct TimelineQuery {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -688,6 +702,8 @@ pub struct TimelineQuery {
|
||||||
pub before: usize,
|
pub before: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub responses_only: bool,
|
pub responses_only: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub order: TimelineOrderMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn swiss_army_timeline(
|
async fn swiss_army_timeline(
|
||||||
|
@ -737,7 +753,13 @@ async fn swiss_army_timeline(
|
||||||
.get_responses_by_user(req.user_id, 12, req.page)
|
.get_responses_by_user(req.user_id, 12, req.page)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
|
if req.order == TimelineOrderMode::Recent {
|
||||||
data.0.get_posts_by_user(req.user_id, 12, req.page).await
|
data.0.get_posts_by_user(req.user_id, 12, req.page).await
|
||||||
|
} else {
|
||||||
|
data.0
|
||||||
|
.get_popular_posts_by_user(req.user_id, 12, req.page)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if req.responses_only {
|
if req.responses_only {
|
||||||
|
|
|
@ -16,7 +16,7 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use crate::cookie::CookieJar;
|
use crate::{cookie::CookieJar, routes::pages::misc::TimelineOrderMode};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tetratto_core::model::{Error, auth::User};
|
use tetratto_core::model::{Error, auth::User};
|
||||||
use crate::{assets::initial_context, get_lang, InnerState};
|
use crate::{assets::initial_context, get_lang, InnerState};
|
||||||
|
@ -222,6 +222,8 @@ pub struct ProfileQuery {
|
||||||
pub responses_only: bool,
|
pub responses_only: bool,
|
||||||
#[serde(default, alias = "f")]
|
#[serde(default, alias = "f")]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
|
#[serde(default, alias = "o")]
|
||||||
|
pub order: TimelineOrderMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -380,6 +380,7 @@ pub async fn posts_request(
|
||||||
context.insert("pinned", &pinned);
|
context.insert("pinned", &pinned);
|
||||||
context.insert("page", &props.page);
|
context.insert("page", &props.page);
|
||||||
context.insert("tag", &props.tag);
|
context.insert("tag", &props.tag);
|
||||||
|
context.insert("order", &props.order);
|
||||||
profile_context(
|
profile_context(
|
||||||
&mut context,
|
&mut context,
|
||||||
&user,
|
&user,
|
||||||
|
|
|
@ -783,6 +783,37 @@ impl DataManager {
|
||||||
Ok(res.unwrap())
|
Ok(res.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all posts from the given user (sorted by likes - dislikes).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `id` - the ID of the user the requested posts belong to
|
||||||
|
/// * `batch` - the limit of posts in each page
|
||||||
|
/// * `page` - the page number
|
||||||
|
pub async fn get_popular_posts_by_user(
|
||||||
|
&self,
|
||||||
|
id: usize,
|
||||||
|
batch: usize,
|
||||||
|
page: usize,
|
||||||
|
) -> Result<Vec<Post>> {
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = query_rows!(
|
||||||
|
&conn,
|
||||||
|
"SELECT * FROM posts WHERE owner = $1 AND replying_to = 0 AND NOT (context::json->>'is_profile_pinned')::boolean AND is_deleted = 0 ORDER BY (likes - dislikes) DESC, created DESC LIMIT $2 OFFSET $3",
|
||||||
|
&[&(id as i64), &(batch as i64), &((page * batch) as i64)],
|
||||||
|
|x| { Self::get_post_from_row(x) }
|
||||||
|
);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
return Err(Error::GeneralNotFound("post".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all posts (that are answering a question) from the given user (from most recent).
|
/// Get all posts (that are answering a question) from the given user (from most recent).
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue