better profile w/ bio & following/followers (placeholder post count)

This commit is contained in:
emilis 2022-09-13 22:09:00 +01:00
parent 2dbc6ad68a
commit 9606eea9e8
7 changed files with 186 additions and 37 deletions

View File

@ -19,3 +19,20 @@ CREATE TABLE keys (
key TEXT NOT NULL PRIMARY KEY, key TEXT NOT NULL PRIMARY KEY,
value TEXT NOT NULL value TEXT NOT NULL
); );
CREATE TABLE follows (
user_id CHAR(22) NOT NULL REFERENCES users(id),
follows_id CHAR(22) NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, follows_id)
);
CREATE TABLE follow_requests(
user_id CHAR(22) NOT NULL REFERENCES users(id),
follows_id CHAR(22) NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, follows_id)
);

View File

@ -23,22 +23,26 @@ impl Users {
Ok(User::from(&row)) Ok(User::from(&row))
} }
pub async fn user_stats(&self, by: UserSelect) -> Result<UserStats, db::DBError> {
let (clause, param) = by.into();
let rows = self
.0
.lock()
.await
.query(
format!(r#"select count(follows.*) as following, count(followed.*) as followers from users
left join follows on follows.user_id = users.id
left join follows followed on followed.follows_id = users.id
where {}"#, clause).as_str(),
&[&param],
)
.await?;
Ok(rows.first().ok_or(db::DBError::NotFound)?.into())
}
pub async fn user(&self, by: UserSelect) -> Result<User, db::DBError> { pub async fn user(&self, by: UserSelect) -> Result<User, db::DBError> {
let where_param: String; let (clause, param) = by.into();
let where_clause = match by {
UserSelect::ID(id) => {
where_param = id;
"id = $1"
}
UserSelect::Username(username) => {
where_param = username;
"username = $1"
}
UserSelect::FullUsername(full) => {
where_param = full;
"(username || '@' || host) = $1"
}
};
let rows = self let rows = self
.0 .0
.lock() .lock()
@ -46,10 +50,10 @@ impl Users {
.query( .query(
format!( format!(
"select id, username, host, display_name, password_hash, email, avatar_uri, bio from users where {}", "select id, username, host, display_name, password_hash, email, avatar_uri, bio from users where {}",
where_clause clause,
) )
.as_str(), .as_str(),
&[&where_param], &[&param],
) )
.await?; .await?;
@ -61,6 +65,7 @@ impl Users {
} }
} }
#[derive(Debug, Clone)]
pub struct User { pub struct User {
pub id: String, pub id: String,
pub username: String, pub username: String,
@ -92,3 +97,51 @@ pub enum UserSelect {
Username(String), Username(String),
FullUsername(String), FullUsername(String),
} }
impl From<String> for UserSelect {
fn from(username: String) -> Self {
if !username.contains("@") {
Self::Username(username)
} else {
Self::FullUsername(username)
}
}
}
impl Into<(String, String)> for UserSelect {
fn into(self) -> (String, String) {
let where_param: String;
let where_clause = match self {
UserSelect::ID(id) => {
where_param = id;
"users.id = $1"
}
UserSelect::Username(username) => {
where_param = username;
"users.username = $1"
}
UserSelect::FullUsername(full) => {
where_param = full;
"(users.username || '@' || users.host) = $1"
}
};
(where_clause.to_owned(), where_param)
}
}
#[derive(Debug, Clone)]
pub struct UserStats {
pub post_count: i64,
pub following: i64,
pub followers: i64,
}
impl From<&Row> for UserStats {
fn from(row: &Row) -> Self {
Self {
post_count: 100,
following: row.get("following"),
followers: row.get("followers"),
}
}
}

View File

@ -42,7 +42,7 @@ impl Server {
} }
fn from_cookies(&self, cookies: Cookies) -> Result<WithNav<Option<Claims>>, ServerError> { fn from_cookies(&self, cookies: Cookies) -> Result<WithNav<Option<Claims>>, ServerError> {
const logged_out: Result<WithNav<Option<Claims>>, ServerError> = Ok(WithNav { const LOGGED_OUT: Result<WithNav<Option<Claims>>, ServerError> = Ok(WithNav {
obj: None, obj: None,
nav_type: NavType::LoggedOut, nav_type: NavType::LoggedOut,
}); });
@ -52,7 +52,7 @@ impl Server {
Err(e) => { Err(e) => {
if e.clone().expired() { if e.clone().expired() {
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, "")); cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
return logged_out; return LOGGED_OUT;
} else { } else {
return Err(e.into()); return Err(e.into());
} }
@ -60,7 +60,7 @@ impl Server {
}; };
Ok(WithNav::new(Some(claims.clone()), claims.into())) Ok(WithNav::new(Some(claims.clone()), claims.into()))
} else { } else {
logged_out LOGGED_OUT
} }
} }

View File

@ -1,10 +1,10 @@
use core::panic; use core::panic;
use std::{convert::Infallible, fmt::Display, net::SocketAddr}; use std::{fmt::Display, net::SocketAddr};
use axum::{ use axum::{
http::{uri::InvalidUri, StatusCode}, http::StatusCode,
response::{self, IntoResponse, Response}, response::{self, IntoResponse},
routing, Extension, Router, Extension, Router,
}; };
use handlebars::{Handlebars, RenderError}; use handlebars::{Handlebars, RenderError};
use tower_cookies::CookieManagerLayer; use tower_cookies::CookieManagerLayer;

View File

@ -18,13 +18,20 @@ impl Profiler {
Self { db } Self { db }
} }
pub async fn profile(&self, username: String) -> Result<User, UserError> { pub async fn user(&self, username: String) -> Result<User, UserError> {
let select = if username.contains("@") { Ok(self.db.user(username.into()).await?.into())
UserSelect::FullUsername(username) }
} else {
UserSelect::Username(username) pub async fn stats(&self, username: String) -> Result<UserStats, UserError> {
}; Ok(self.db.user_stats(username.into()).await?.into())
Ok(User::from(self.db.user(select).await?)) }
pub async fn profile(&self, username: String) -> Result<Profile, UserError> {
Ok((
self.user(username.clone()).await?,
self.stats(username).await?,
)
.into())
} }
pub async fn create_user( pub async fn create_user(
@ -61,6 +68,38 @@ pub struct User {
pub bio: Option<String>, pub bio: Option<String>,
} }
#[derive(Debug, Clone, Serialize)]
pub struct UserStats {
pub post_count: i64,
pub following: i64,
pub followers: i64,
}
impl From<users::UserStats> for UserStats {
fn from(u: users::UserStats) -> Self {
Self {
post_count: u.post_count,
following: u.following,
followers: u.followers,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Profile {
pub user: User,
pub stats: UserStats,
}
impl From<(User, UserStats)> for Profile {
fn from((user, stats): (User, UserStats)) -> Self {
Self {
user: user.into(),
stats: stats.into(),
}
}
}
impl From<users::User> for User { impl From<users::User> for User {
fn from(u: users::User) -> Self { fn from(u: users::User) -> Self {
Self { Self {

View File

@ -97,9 +97,35 @@ a {
} }
.profile-bio { .profile-bio {
border: 3px solid rebeccapurple; border: 3px solid rgba(102, 51, 153, 0.5);
margin: auto; margin-left: 5px;
text-align: center; text-align: center;
height: fit-content;
height: -moz-fit-content;
margin-left: 10px;
}
.stats>div {
border: 3px solid rgba(102, 51, 153, 0.5);
height: fit-content;
height: -moz-fit-content;
text-align: center;
width: 33%;
float: left;
margin: 5px;
}
.stats {
height: fit-content;
margin: 5px;
overflow: hidden;
width: auto;
display: flex;
align-items: center;
}
.stat-name {
font-weight: bold;
} }
nav { nav {

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<title>@{{obj.username}}</title> <title>@{{obj.user.username}}</title>
<link rel="stylesheet" href="/static/style/main.css"> <link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head> </head>
@ -12,11 +12,25 @@
<div class="main"> <div class="main">
<div class="profile"> <div class="profile">
<div class="profile-header"> <div class="profile-header">
<img class="avatar" src="{{obj.avatar_uri}}" alt="{{obj.username}}'s avatar" /> <img class="avatar" src="{{obj.user.avatar_uri}}" alt="{{obj.user.username}}'s avatar" />
<p class="name">{{obj.display_name}}</p> <p class="name">{{obj.user.display_name}}</p>
</div> </div>
<div class="profile-bio"> <div class="profile-bio">
<p>{{obj.bio}}</p> <p>{{obj.user.bio}}</p>
<div class="stats">
<div id="posts">
<p class="stat-name">posts</p>
<p>{{obj.stats.post_count}}</p>
</div>
<div id="following">
<p class="stat-name">following</p>
<p>{{obj.stats.following}}</p>
</div>
<div id="followers">
<p class="stat-name">followers</p>
<p>{{obj.stats.followers}}</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>