feat follow companies from your profile

users
Miroito 2 years ago
parent 68e77ea0fa
commit 38b3797304

@ -1,8 +1,18 @@
use crate::api::{types::user::UserProfile, FastInsidersApi};
use crate::api::{
types::{
company::Company,
paginated_response::PaginatedResponse,
user::{FollowCompany, UserProfile},
},
FastInsidersApi,
};
#[cfg(client)]
use super::user::set_token_cookie;
#[cfg(client)]
use serde::Serialize;
#[cfg(engine)]
type Response = reqwest::Response;
@ -28,6 +38,23 @@ async fn get_auth_route(route: &str) -> Result<Response, ()> {
Ok(resp)
}
#[cfg(client)]
async fn post_auth_route<T>(route: &str, body: T) -> Result<Response, ()>
where
T: Serialize,
{
let token = wasm_cookies::get_raw("token").unwrap_or_default();
let resp = reqwasm::http::Request::post(route)
.header("Authorization", &format!("Bearer {}", token))
.header("Content-type", "application/json")
.body(serde_json::to_string(&body).unwrap())
.send()
.await
.map_err(|_| ())?;
update_token(&resp);
Ok(resp)
}
impl FastInsidersApi {
/// This is only a route to verify that we are authenticated
pub async fn is_authenticated(&self) -> Result<Response, ()> {
@ -65,4 +92,49 @@ impl FastInsidersApi {
Ok(res)
}
pub async fn follow_company(&self, company_id: i32) -> Result<(), ()> {
let route = &format!("{}/auth/follow_company", self.url);
#[cfg(client)]
let res = {
let body = FollowCompany { company_id };
let resp = post_auth_route(route, body).await?;
};
#[cfg(engine)]
return Err(());
Ok(())
}
pub async fn get_followed_companies(
&self,
page: i64,
size: i64,
) -> Result<PaginatedResponse<Company>, ()> {
let route = &format!(
"{}/auth/get_followed_companies?page={}&size={}",
self.url, page, size
);
#[cfg(client)]
let res = {
let resp = get_auth_route(route).await?;
resp.json::<PaginatedResponse<Company>>()
.await
.map_err(|_| ())?
};
#[cfg(engine)]
#[cfg(engine)]
let res = reqwest::get(route)
.await
.map_err(|_| ())?
.json::<PaginatedResponse<Company>>()
.await
.map_err(|_| ())?;
Ok(res)
}
}

@ -3,7 +3,7 @@ use sycamore::prelude::*;
use crate::components::base_table::TableContent;
use super::transaction::{TransactionCompany, TransactionsAggregated, LatestTransaction, MajorTransactions};
use super::{transaction::{TransactionCompany, TransactionsAggregated, LatestTransaction, MajorTransactions}, company::Company};
pub trait IntoTableData<G>
where
@ -190,3 +190,36 @@ where
}
}
}
impl<G> IntoTableData<G> for PaginatedResponse<Company>
where
G: GenericNode,
{
fn into_table_data(self, cx: Scope) -> TableContent<G> {
let headers_view = vec![
view! {cx, "Company" },
];
let data_view: Vec<Vec<View<G>>> = self
.list
.into_iter()
.map(|t| {
let mut res = vec![];
res.push(view! {cx,
a (href=format!("transactions/{}", t.slug),
class="text-indigo-800 dark:text-indigo-300 hover:text-indigo-500 hover:underline dark:hover:text-indigo-600",
) {
(t.name.to_owned())
}
});
res
})
.collect();
TableContent {
headers_view,
data_view,
}
}
}

@ -1,7 +1,12 @@
use serde::Deserialize;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
pub struct UserProfile {
pub email: Option<String>,
pub name: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct FollowCompany {
pub company_id: i32,
}

@ -17,6 +17,7 @@ where
{
pub route: &'a R,
pub selected_item: &'a Signal<Option<T>>,
pub clear: &'a Signal<bool>,
}
#[component]
@ -62,6 +63,14 @@ where
});
});
create_effect(cx, move || {
if *props.clear.get() {
props.clear.set(false);
props.selected_item.set(None);
input.set("".to_string());
}
});
view! { cx,
input (bind:value=input, class="p-2 w-full rounded-md bg-slate-300 dark:bg-slate-800", on:blur=hide_dropdown) {}
div (class="relative") {

@ -21,6 +21,7 @@ where
pub route: &'a C,
pub filter: Option<String>,
pub table_class: &'a String,
pub refresh: &'a Signal<bool>,
}
impl<'a, M, F, C> PaginatedTableStateRx<'a, M, F, C>
@ -81,21 +82,39 @@ where
page_size_string.track();
page.set(0);
});
let props_sig = create_signal(cx, props);
#[cfg(client)]
create_effect(cx, move || {
let page = *page.get();
let page_size_s = page_size_string.get();
let page_size = page_size_s.parse().unwrap_or(20);
let data_fetch = move || {
spawn_local_scoped(cx, async move {
let res = props_sig.get().get_data(page, page_size).await.unwrap();
let res = props_sig
.get()
.get_data(*page.get(), page_size_string.get().parse().unwrap_or(20))
.await
.unwrap();
paginated_data.set(Some(res.clone()));
n_rows.set(res.count);
let table_content = res.into_table_data(cx);
table_prop.data_view.set(table_content.data_view);
table_prop.headers_view.set(table_content.headers_view);
n_page.set((*paginated_data.get()).as_ref().map_or(0, |t| t.num_pages));
});
})
};
#[cfg(client)]
create_effect(cx, move || {
if *props_sig.get().refresh.get() {
props_sig.get().refresh.set(false);
data_fetch()
}
});
#[cfg(client)]
create_effect(cx, move || {
page.track();
page_size_string.track();
data_fetch();
});
view! { cx,

@ -29,6 +29,7 @@ fn index_page<G: Html>(cx: BoundedScope) -> View<G> {
route: route_ref,
filter: Some("72".to_string()),
table_class: table_classes,
refresh: create_signal(cx, true),
};
let route_ref = create_ref(cx, move |c, p, s| {
@ -40,6 +41,7 @@ fn index_page<G: Html>(cx: BoundedScope) -> View<G> {
route: route_ref,
filter: Some((24 * 30).to_string()),
table_class: table_classes,
refresh: create_signal(cx, true),
};
let route_ref = create_ref(cx, move |c, p, s| {
@ -51,6 +53,7 @@ fn index_page<G: Html>(cx: BoundedScope) -> View<G> {
route: route_ref,
filter: Some((24 * 30).to_string()),
table_class: table_classes,
refresh: create_signal(cx, true),
};
let dark_mode_class = create_memo(cx, || {

@ -1,9 +1,17 @@
use std::rc::Rc;
use perseus::prelude::*;
use sycamore::{prelude::*, rt::Event};
use crate::{
components::{
loading::Loading, main_content_container::MainContentContainer, the_header::TheHeader,
base_async_select::{AsyncSelectRx, BaseAsyncSelect},
base_button::{BaseButton, BaseButtonStateRx},
base_table::BaseTable,
loading::Loading,
main_content_container::MainContentContainer,
paginated_data_table::{PaginatedTable, PaginatedTableStateRx},
the_header::TheHeader,
},
global_state::AppStateRx,
};
@ -46,15 +54,53 @@ fn profile_page<G: Html>(cx: BoundedScope) -> View<G> {
}
});
let async_select_route_ref = create_ref(cx, |n, l| api_scope_ref.get_company_by_name(n, l));
let async_select_prop: AsyncSelectRx<_, _, _> = AsyncSelectRx {
route: async_select_route_ref,
selected_item: create_signal(cx, None),
clear: create_signal(cx, false),
};
let table_route_ref = create_ref(cx, move |_, p, s| {
api_scope_ref.get_followed_companies(p, s)
});
let paginated_table_state: PaginatedTableStateRx<_, _, _> = PaginatedTableStateRx {
record_label: "Companies".to_owned(),
route: table_route_ref,
filter: None,
table_class: create_ref(cx, "w-full".to_string()),
refresh: create_signal(cx, true),
};
let follow_button = BaseButtonStateRx {
label: create_signal(cx, "Follow".to_string()),
disabled: create_memo(cx, move || async_select_prop.selected_item.get().is_none()),
clicked: create_signal(cx, false),
};
create_effect(cx, move || {
if *follow_button.clicked.get() && async_select_prop.selected_item.get_untracked().is_some()
{
follow_button.clicked.set(false);
if let Some(company) = (*async_select_prop.selected_item.get_untracked()).clone() {
spawn_local_scoped(cx, async move {
api_scope_ref.follow_company(company.id).await.unwrap();
paginated_table_state.refresh.set(true);
});
}
async_select_prop.clear.set(true);
}
});
view! {cx,
main (class=format!("{} flex flex-1", dark_mode_class)) {
div (class="flex-1 font-sans bg-slate-200 text-slate-700 dark:bg-slate-700 dark:text-slate-100") {
TheHeader()
MainContentContainer(useless_prop=1) {
div(class="items-center m-auto w-2/5 min-w-70") {
h1(class="text-lg text-center") {
"Profile page"
}
div(class="m-auto w-full max-w-2xl") {
(if !*loading.get() {
view! {cx,
p() {
@ -83,7 +129,13 @@ fn profile_page<G: Html>(cx: BoundedScope) -> View<G> {
Loading()
}
})
h2(class="mt-3 font-bold") {
"Follow companies"
}
BaseAsyncSelect(async_select_prop)
BaseButton(follow_button)
PaginatedTable(paginated_table_state)
}
}
}
}

@ -45,12 +45,14 @@ fn transactions_page<'a, G: Html>(cx: Scope, state: &TransactionsPageStateRx) ->
route: route_ref,
filter: (*state.company_slug.get()).clone(),
table_class: create_ref(cx, "".to_string()),
refresh: create_signal(cx, true),
};
let route_ref = create_ref(cx, |n, l| api_scope_ref.get_company_by_name(n, l));
let async_select_prop: AsyncSelectRx<_, _, _> = AsyncSelectRx {
route: route_ref,
selected_item: create_signal(cx, None),
clear: create_signal(cx, false),
};
let search_button = BaseButtonStateRx {

File diff suppressed because one or more lines are too long

@ -23,7 +23,7 @@ impl MigrationTrait for Migration {
.foreign_key(
ForeignKey::create()
.name("FK_user")
.from(UserCompany::Table, UserCompany::CompanyId)
.from(UserCompany::Table, UserCompany::UserId)
.to(user::User::Table, user::User::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade),

@ -48,6 +48,7 @@ where
Ok(res)
}
/// Use for 1-1 relationships, retrieves the entity and the single related record
pub async fn paginate_also_related<E, R, T, K, C>(
db: &DatabaseConnection,
page: Option<u64>,

@ -46,7 +46,10 @@ mod task;
use crate::{
crypto::{jwt_numeric_date, JWTSecretManager},
route::{
authenticated::{get_profile, is_authenticated},
authenticated::{
follow_company_route, get_followed_companies, get_profile, is_authenticated,
unfollow_company_route,
},
user,
},
task::run_tasks,
@ -191,6 +194,9 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
let authenticated_routes = Router::<AppState, _>::new()
.route("/is_authenticated", get(is_authenticated))
.route("/profile", get(get_profile))
.route("/get_followed_companies", get(get_followed_companies))
.route("/follow_company", post(follow_company_route))
.route("/unfollow_company", post(unfollow_company_route))
.layer(from_fn_with_state(shared_state.clone(), authenticator))
.with_state(shared_state.clone());

@ -24,7 +24,7 @@ pub enum Relation {
Company,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::CompanyId",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"

@ -1,5 +1,8 @@
use crate::model::user::ActiveModel;
use sea_orm::{ActiveModelTrait, ConnectionTrait, DbErr, DeriveIntoActiveModel, IntoActiveModel};
use sea_orm::{
ActiveModelTrait, ActiveValue, ConnectionTrait, DbErr, DeriveIntoActiveModel, EntityTrait,
IntoActiveModel,
};
use serde::{Deserialize, Serialize};
use crate::model;
@ -21,3 +24,28 @@ impl NewUser {
Ok(res)
}
}
pub async fn follow_company<C>(db: &C, user_id: i32, company_id: i32) -> Result<(), DbErr>
where
C: ConnectionTrait,
{
let relation = model::user_company::ActiveModel {
user_id: ActiveValue::Set(user_id),
company_id: ActiveValue::Set(company_id),
};
relation.insert(db).await?;
Ok(())
}
pub async fn unfollow_company<C>(db: &C, user_id: i32, company_id: i32) -> Result<(), DbErr>
where
C: ConnectionTrait,
{
model::user_company::Entity::delete_by_id((user_id, company_id))
.exec(db)
.await?;
Ok(())
}

@ -1,8 +1,24 @@
use axum::{extract::State, Extension, Json};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use axum::{
extract::{Query, State},
Extension, Json,
};
use sea_orm::{
ColumnTrait, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect,
};
use serde::{Deserialize, Serialize};
use crate::{error::AppError, model, AppState, UserJWTClaim};
use crate::{
db::paginate::PaginatedResponse,
error::AppError,
model::{
self,
prelude::{Company, User},
},
repo::user::{follow_company, unfollow_company},
AppState, UserJWTClaim,
};
use super::Pagination;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserProfile {
@ -44,3 +60,65 @@ pub async fn get_profile(
"Authenticated user does not exist".to_string(),
))
}
#[derive(Deserialize)]
pub struct FollowCompany {
company_id: i32,
}
pub async fn follow_company_route(
Extension(token_data): Extension<UserJWTClaim>,
State(state): State<AppState>,
Json(payload): Json<FollowCompany>,
) -> Result<(), AppError> {
let db = &state.db;
follow_company(db, token_data.user_id, payload.company_id).await?;
Ok(())
}
pub async fn unfollow_company_route(
Extension(token_data): Extension<UserJWTClaim>,
State(state): State<AppState>,
Json(payload): Json<FollowCompany>,
) -> Result<(), AppError> {
let db = &state.db;
unfollow_company(db, token_data.user_id, payload.company_id).await?;
Ok(())
}
pub async fn get_followed_companies(
Extension(token_data): Extension<UserJWTClaim>,
State(state): State<AppState>,
Query(Pagination { page, size }): Query<Pagination>,
) -> Result<Json<PaginatedResponse<model::company::Model>>, AppError> {
let db = &state.db;
let s = size.unwrap_or(10);
let p = page.unwrap_or_default();
let user = User::find_by_id(token_data.user_id)
.one(db)
.await?
.ok_or(AppError::NotFound("User not found".to_string()))?;
let (count_res, companies) = tokio::join!(user.find_related(Company).count(db), async {
user.find_related(Company)
.order_by(model::company::Column::Name, sea_orm::Order::Asc)
.offset(p * s)
.limit(s)
.all(db)
.await
});
let count = count_res?;
let res = PaginatedResponse {
count,
num_pages: count / s + 1,
list: companies?,
};
Ok(Json(res))
}

Loading…
Cancel
Save