add: user settings

fix: actually use cached stuff in auto_method macro
add: profile ui base
This commit is contained in:
trisua 2025-03-25 23:58:27 -04:00
parent 8580e34be2
commit 7d96a3d20f
16 changed files with 222 additions and 8 deletions

View file

@ -36,6 +36,9 @@ pub const AUTH_BASE: &str = include_str!("./public/html/auth/base.html");
pub const AUTH_LOGIN: &str = include_str!("./public/html/auth/login.html");
pub const AUTH_REGISTER: &str = include_str!("./public/html/auth/register.html");
pub const PROFILE_BASE: &str = include_str!("./public/html/profile/base.html");
pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.html");
// langs
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
@ -137,6 +140,9 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
write_template!(html_path->"auth/login.html"(crate::assets::AUTH_LOGIN) --config=config);
write_template!(html_path->"auth/register.html"(crate::assets::AUTH_REGISTER) --config=config);
write_template!(html_path->"profile/base.html"(crate::assets::PROFILE_BASE) -d "profile" --config=config);
write_template!(html_path->"profile/posts.html"(crate::assets::PROFILE_POSTS) --config=config);
html_path
}

View file

@ -15,3 +15,6 @@ version = "1.0.0"
"auth:action.logout" = "Logout"
"auth:link.my_profile" = "My profile"
"auth:link.settings" = "Settings"
"auth:label.followers" = "Followers"
"auth:label.following" = "Following"
"auth:label.joined_journals" = "Joined Journals"

View file

@ -88,6 +88,10 @@ footer {
padding: 0.75rem 1rem;
}
article {
margin-top: 1rem;
}
@media screen and (max-width: 900px) {
main,
article,
@ -96,6 +100,10 @@ footer {
footer {
width: 100%;
}
article {
margin-top: 0;
}
}
.content_container {
@ -135,6 +143,8 @@ footer {
svg.icon {
stroke: currentColor;
width: 18px;
width: 1em;
height: 1em;
}
svg.icon.filled {
@ -360,6 +370,14 @@ button,
font-weight: 600;
}
button.small,
.button.small {
min-height: max-content;
padding: 0.25rem;
height: 24px;
font-size: 16px;
}
button:hover,
.button:hover {
background: var(--color-primary-lowered);
@ -490,6 +508,15 @@ select:focus {
padding: 0;
}
/* chip */
.chip {
background: var(--color-primary);
color: var(--color-text-primary);
font-weight: 600;
border-radius: var(--circle);
padding: 0.05rem 0.75rem;
}
/* nav */
nav {
background: var(--color-primary);
@ -1041,6 +1068,7 @@ details.accordion .inner {
.sm\:w-full {
width: 100% !important;
min-width: 100% !important;
}
.sm\:mt-2 {

View file

@ -33,7 +33,7 @@
<div class="inner">
<b class="title">{{ user.username }}</b>
<a href="/@{{ user.username }}">
<a href="/user/{{ user.username }}">
{{ icon "book-heart" }}
<span>{{ text "auth:link.my_profile" }}</span>
</a>

View file

@ -1,6 +1,5 @@
{% import "macros.html" as macros %} {% extends "root.html" %} {% block body %}
{{ macros::nav(selected="home") }}
<main class="flex flex-col gap-2">
<div class="card-nest">
<div class="card">

View file

@ -0,0 +1,80 @@
{% import "macros.html" as macros %} {% extends "root.html" %} {% block head %}
<title>{{ profile.username }} - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<article>
<div class="content_container">
<div class="w-full flex gap-4 flex-collapse">
<div
class="lhs flex flex-col gap-2 sm:w-full"
style="min-width: 20rem"
>
<div class="card-nest w-full">
<div class="card flex gap-2" id="user_avatar_and_name">
{{ macros::avatar(username=profile.username,size="72px")
}}
<div class="flex flex-col">
<h3 id="username">
{% if profile.settings.display_name %} {{
profile.settings.display_name }} {% else %} {{
profile.username }} {% endif %}
</h3>
<span class="fade">{{ profile.username }}</span>
</div>
</div>
<div class="card flex" id="social">
<div
class="w-full flex justify-center items-center gap-2"
>
<h4>{{ profile.follower_count }}</h4>
<span>{{ text "auth:label.followers" }}</span>
</div>
<div
class="w-full flex justify-center items-center gap-2"
>
<h4>{{ profile.following_count }}</h4>
<span>{{ text "auth:label.following" }}</span>
</div>
</div>
</div>
<div class="card-nest flex flex-col">
<div id="bio" class="card">
{{ profile.settings.biography }}
</div>
<div class="card flex flex-col gap-2">
<div class="w-full flex justify-between items-center">
<span class="notification chip">ID</span>
<button
title="Copy"
onclick="trigger('atto::copy_text', [{{ profile.id }}])"
class="camo small"
>
{{ icon "copy" }}
</button>
</div>
<div class="w-full flex justify-between items-center">
<span class="notification chip">Joined</span>
<span class="date">{{ profile.created }}</span>
</div>
</div>
</div>
<div class="card-nest">
<div class="card flex gap-2 items-center">
{{ icon "users-round" }}
<span>{{ text "auth:label.joined_journals" }}</span>
</div>
<div class="card flex flex-wrap gap-2"></div>
</div>
</div>
<div class="rhs sm:w-full">{% block content %}{% endblock %}</div>
</div>
</div>
</article>
{% endblock %}

View file

@ -0,0 +1,2 @@
{% import "macros.html" as macros %} {% extends "profile/base.html" %} {% block
content %}<span></span>{% endblock %}

View file

@ -56,7 +56,9 @@
<body>
<div id="toast_zone"></div>
{% block body %}{% endblock %}
<div id="page" style="display: contents">
{% block body %}{% endblock %}
</div>
<script data-turbo-permanent="true" id="init-script">
document.documentElement.addEventListener("turbo:load", () => {

View file

@ -1,4 +1,5 @@
pub mod images;
pub mod profile;
pub mod social;
use super::AuthProps;

View file

@ -0,0 +1,36 @@
use crate::{
State, get_user_from_token,
model::{ApiReturn, Error},
};
use axum::{Extension, Json, extract::Path, response::IntoResponse};
use axum_extra::extract::CookieJar;
use tetratto_core::model::{auth::UserSettings, permissions::FinePermission};
/// Update the settings of the given user.
pub async fn update_profile_settings_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
Json(req): Json<UserSettings>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
if user.id != id {
if !user.permissions.check(FinePermission::MANAGE_USERS) {
return Json(Error::NotAllowed.into());
}
}
match data.update_user_settings(id, req).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "User unfollowed".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -78,6 +78,10 @@ pub fn routes() -> Router {
"/auth/profile/{id}/block",
post(auth::social::block_request),
)
.route(
"/auth/profile/{id}/settings",
post(auth::profile::update_profile_settings_request),
)
}
#[derive(Deserialize)]

View file

@ -1,5 +1,6 @@
pub mod auth;
pub mod misc;
pub mod profile;
use axum::{Router, routing::get};
@ -10,4 +11,6 @@ pub fn routes() -> Router {
// auth
.route("/auth/register", get(auth::register_request))
.route("/auth/login", get(auth::login_request))
// profile
.route("/user/{username}", get(profile::posts_request))
}

View file

@ -0,0 +1,30 @@
use crate::{State, assets::initial_context, get_lang, get_user_from_token};
use axum::{
Extension,
extract::Path,
response::{Html, IntoResponse},
};
use axum_extra::extract::CookieJar;
/// `/user/{username}`
pub async fn posts_request(
jar: CookieJar,
Path(username): Path<String>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = data.read().await;
let user = get_user_from_token!(jar, data.0);
let other_user = match data.0.get_user_by_username(&username).await {
Ok(ua) => ua,
Err(e) => return Err(Html(e.to_string())),
};
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0, lang, &user);
context.insert("profile", &other_user);
Ok(Html(
data.1.render("profile/posts.html", &mut context).unwrap(),
))
}

View file

@ -2,7 +2,7 @@ use super::*;
use crate::cache::Cache;
use crate::model::{
Error, Result,
auth::{Token, User},
auth::{Token, User, UserSettings},
permissions::FinePermission,
};
use crate::{auto_method, execute, get, query_row};
@ -145,6 +145,7 @@ impl DataManager {
}
auto_method!(update_user_tokens(Vec<Token>) -> "UPDATE users SET tokens = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.user:{}");
auto_method!(update_user_settings(UserSettings) -> "UPDATE users SET settings = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.user:{}");
auto_method!(incr_user_notifications() -> "UPDATE users SET notification_count = notification_count + 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --incr);
auto_method!(decr_user_notifications() -> "UPDATE users SET notification_count = notification_count - 1 WHERE id = $1" --cache-key-tmpl="atto.user:{}" --decr);

View file

@ -50,6 +50,10 @@ macro_rules! auto_method {
($name:ident()@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt --cache-key-tmpl=$cache_key_tmpl:literal) => {
pub async fn $name(&self, id: usize) -> Result<$returns_> {
if let Some(cached) = self.2.get(format!($cache_key_tmpl, id)).await {
return Ok(serde_json::from_str(&cached).unwrap());
}
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@ -94,6 +98,10 @@ macro_rules! auto_method {
($name:ident($selector_t:ty)@$select_fn:ident -> $query:literal --name=$name_:literal --returns=$returns_:tt --cache-key-tmpl=$cache_key_tmpl:literal) => {
pub async fn $name(&self, selector: $selector_t) -> Result<$returns_> {
if let Some(cached) = self.2.get(format!($cache_key_tmpl, selector)).await {
return Ok(serde_json::from_str(&cached).unwrap());
}
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),

View file

@ -9,7 +9,7 @@ use tetratto_shared::{
/// `(ip, token, creation timestamp)`
pub type Token = (String, String, usize);
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct User {
pub id: usize,
pub created: usize,
@ -25,11 +25,22 @@ pub struct User {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserSettings;
pub struct UserSettings {
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub biography: String,
#[serde(default)]
pub private_profile: bool,
}
impl Default for UserSettings {
fn default() -> Self {
Self {}
Self {
display_name: String::new(),
biography: String::new(),
private_profile: false,
}
}
}
@ -79,7 +90,7 @@ impl User {
}
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct Notification {
pub id: usize,
pub created: usize,