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