better profile w/ bio & following/followers (placeholder post count)
This commit is contained in:
		
							parent
							
								
									2dbc6ad68a
								
							
						
					
					
						commit
						9606eea9e8
					
				| 
						 | 
				
			
			@ -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)
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(),
 | 
			
		||||
                &[¶m],
 | 
			
		||||
            )
 | 
			
		||||
            .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],
 | 
			
		||||
                &[¶m],
 | 
			
		||||
            )
 | 
			
		||||
            .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"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue