add: user settings
fix: actually use cached stuff in auto_method macro add: profile ui base
This commit is contained in:
parent
8580e34be2
commit
7d96a3d20f
16 changed files with 222 additions and 8 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
80
crates/app/src/public/html/profile/base.html
Normal file
80
crates/app/src/public/html/profile/base.html
Normal 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 %}
|
2
crates/app/src/public/html/profile/posts.html
Normal file
2
crates/app/src/public/html/profile/posts.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
{% import "macros.html" as macros %} {% extends "profile/base.html" %} {% block
|
||||
content %}<span></span>{% endblock %}
|
|
@ -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", () => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod images;
|
||||
pub mod profile;
|
||||
pub mod social;
|
||||
|
||||
use super::AuthProps;
|
||||
|
|
36
crates/app/src/routes/api/v1/auth/profile.rs
Normal file
36
crates/app/src/routes/api/v1/auth/profile.rs
Normal 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()),
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
30
crates/app/src/routes/pages/profile.rs
Normal file
30
crates/app/src/routes/pages/profile.rs
Normal 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(),
|
||||
))
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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())),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue