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,
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))
}
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> {
let where_param: String;
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 (clause, param) = by.into();
let rows = self
.0
.lock()
@ -46,10 +50,10 @@ impl Users {
.query(
format!(
"select id, username, host, display_name, password_hash, email, avatar_uri, bio from users where {}",
where_clause
clause,
)
.as_str(),
&[&where_param],
&[&param],
)
.await?;
@ -61,6 +65,7 @@ impl Users {
}
}
#[derive(Debug, Clone)]
pub struct User {
pub id: String,
pub username: String,
@ -92,3 +97,51 @@ pub enum UserSelect {
Username(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> {
const logged_out: Result<WithNav<Option<Claims>>, ServerError> = Ok(WithNav {
const LOGGED_OUT: Result<WithNav<Option<Claims>>, ServerError> = Ok(WithNav {
obj: None,
nav_type: NavType::LoggedOut,
});
@ -52,7 +52,7 @@ impl Server {
Err(e) => {
if e.clone().expired() {
cookies.remove(Cookie::new(AUTH_COOKIE_NAME, ""));
return logged_out;
return LOGGED_OUT;
} else {
return Err(e.into());
}
@ -60,7 +60,7 @@ impl Server {
};
Ok(WithNav::new(Some(claims.clone()), claims.into()))
} else {
logged_out
LOGGED_OUT
}
}

View File

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

View File

@ -18,13 +18,20 @@ impl Profiler {
Self { db }
}
pub async fn profile(&self, username: String) -> Result<User, UserError> {
let select = if username.contains("@") {
UserSelect::FullUsername(username)
} else {
UserSelect::Username(username)
};
Ok(User::from(self.db.user(select).await?))
pub async fn user(&self, username: String) -> Result<User, UserError> {
Ok(self.db.user(username.into()).await?.into())
}
pub async fn stats(&self, username: String) -> Result<UserStats, UserError> {
Ok(self.db.user_stats(username.into()).await?.into())
}
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(
@ -61,6 +68,38 @@ pub struct User {
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 {
fn from(u: users::User) -> Self {
Self {

View File

@ -97,9 +97,35 @@ a {
}
.profile-bio {
border: 3px solid rebeccapurple;
margin: auto;
border: 3px solid rgba(102, 51, 153, 0.5);
margin-left: 5px;
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 {

View File

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