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,
|
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)
|
||||||
|
);
|
||||||
|
|
|
@ -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(),
|
||||||
|
&[¶m],
|
||||||
|
)
|
||||||
|
.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],
|
&[¶m],
|
||||||
)
|
)
|
||||||
.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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue