From 9606eea9e80a61c2af0624ec74748f5c13003bc1 Mon Sep 17 00:00:00 2001 From: emilis Date: Tue, 13 Sep 2022 22:09:00 +0100 Subject: [PATCH] better profile w/ bio & following/followers (placeholder post count) --- migrate/1662887862_schema.up.sql | 17 +++++++ src/database/users.rs | 87 +++++++++++++++++++++++++------- src/servek/html.rs | 6 +-- src/servek/servek.rs | 8 +-- src/svc/profiles.rs | 53 ++++++++++++++++--- static/style/main.css | 30 ++++++++++- templates/html/profile.html | 22 ++++++-- 7 files changed, 186 insertions(+), 37 deletions(-) diff --git a/migrate/1662887862_schema.up.sql b/migrate/1662887862_schema.up.sql index fb2f16b..7fd26b1 100644 --- a/migrate/1662887862_schema.up.sql +++ b/migrate/1662887862_schema.up.sql @@ -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) +); diff --git a/src/database/users.rs b/src/database/users.rs index 9d8cbda..14283ca 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -23,22 +23,26 @@ impl Users { Ok(User::from(&row)) } + pub async fn user_stats(&self, by: UserSelect) -> Result { + 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 { - 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 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"), + } + } +} diff --git a/src/servek/html.rs b/src/servek/html.rs index 6f5ea5a..1bf41ae 100644 --- a/src/servek/html.rs +++ b/src/servek/html.rs @@ -42,7 +42,7 @@ impl Server { } fn from_cookies(&self, cookies: Cookies) -> Result>, ServerError> { - const logged_out: Result>, ServerError> = Ok(WithNav { + const LOGGED_OUT: Result>, 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 } } diff --git a/src/servek/servek.rs b/src/servek/servek.rs index 395f48f..33fb71e 100644 --- a/src/servek/servek.rs +++ b/src/servek/servek.rs @@ -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; diff --git a/src/svc/profiles.rs b/src/svc/profiles.rs index cb7d010..af0e17c 100644 --- a/src/svc/profiles.rs +++ b/src/svc/profiles.rs @@ -18,13 +18,20 @@ impl Profiler { Self { db } } - pub async fn profile(&self, username: String) -> Result { - 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 { + Ok(self.db.user(username.into()).await?.into()) + } + + pub async fn stats(&self, username: String) -> Result { + Ok(self.db.user_stats(username.into()).await?.into()) + } + + pub async fn profile(&self, username: String) -> Result { + 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, } +#[derive(Debug, Clone, Serialize)] +pub struct UserStats { + pub post_count: i64, + pub following: i64, + pub followers: i64, +} + +impl From 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 for User { fn from(u: users::User) -> Self { Self { diff --git a/static/style/main.css b/static/style/main.css index e700525..ad961f7 100644 --- a/static/style/main.css +++ b/static/style/main.css @@ -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 { diff --git a/templates/html/profile.html b/templates/html/profile.html index 011eebe..4d73154 100644 --- a/templates/html/profile.html +++ b/templates/html/profile.html @@ -2,7 +2,7 @@ - @{{obj.username}} + @{{obj.user.username}} @@ -12,11 +12,25 @@
- {{obj.username}}'s avatar -

{{obj.display_name}}

+ {{obj.user.username}}'s avatar +

{{obj.user.display_name}}

-

{{obj.bio}}

+

{{obj.user.bio}}

+
+
+

posts

+

{{obj.stats.post_count}}

+
+
+

following

+

{{obj.stats.following}}

+
+
+

followers

+

{{obj.stats.followers}}

+
+