feat: add activity feed/users/profile use cases and port methods
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use domain::models::{DiaryEntry, collections::Paginated};
|
use domain::models::{DiaryEntry, FeedEntry, MonthActivity, UserStats, UserSummary, UserTrends, collections::Paginated};
|
||||||
|
|
||||||
pub struct HtmlPageContext {
|
pub struct HtmlPageContext {
|
||||||
pub user_email: Option<String>,
|
pub user_email: Option<String>,
|
||||||
@@ -23,11 +23,41 @@ pub struct NewReviewPageData<'a> {
|
|||||||
pub error: Option<&'a str>,
|
pub error: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ActivityFeedPageData {
|
||||||
|
pub ctx: HtmlPageContext,
|
||||||
|
pub entries: Paginated<FeedEntry>,
|
||||||
|
pub current_offset: u32,
|
||||||
|
pub has_more: bool,
|
||||||
|
pub limit: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UsersPageData {
|
||||||
|
pub ctx: HtmlPageContext,
|
||||||
|
pub users: Vec<UserSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProfilePageData {
|
||||||
|
pub ctx: HtmlPageContext,
|
||||||
|
pub profile_user_id: Uuid,
|
||||||
|
pub profile_user_email: String,
|
||||||
|
pub stats: UserStats,
|
||||||
|
pub view: String,
|
||||||
|
pub entries: Option<Paginated<DiaryEntry>>,
|
||||||
|
pub current_offset: u32,
|
||||||
|
pub has_more: bool,
|
||||||
|
pub limit: u32,
|
||||||
|
pub history: Option<Vec<MonthActivity>>,
|
||||||
|
pub trends: Option<UserTrends>,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait HtmlRenderer: Send + Sync {
|
pub trait HtmlRenderer: Send + Sync {
|
||||||
fn render_diary_page(&self, data: &Paginated<DiaryEntry>, ctx: HtmlPageContext) -> Result<String, String>;
|
fn render_diary_page(&self, data: &Paginated<DiaryEntry>, ctx: HtmlPageContext) -> Result<String, String>;
|
||||||
fn render_login_page(&self, data: LoginPageData<'_>) -> Result<String, String>;
|
fn render_login_page(&self, data: LoginPageData<'_>) -> Result<String, String>;
|
||||||
fn render_register_page(&self, data: RegisterPageData<'_>) -> Result<String, String>;
|
fn render_register_page(&self, data: RegisterPageData<'_>) -> Result<String, String>;
|
||||||
fn render_new_review_page(&self, data: NewReviewPageData<'_>) -> Result<String, String>;
|
fn render_new_review_page(&self, data: NewReviewPageData<'_>) -> Result<String, String>;
|
||||||
|
fn render_activity_feed_page(&self, data: ActivityFeedPageData) -> Result<String, String>;
|
||||||
|
fn render_users_page(&self, data: UsersPageData) -> Result<String, String>;
|
||||||
|
fn render_profile_page(&self, data: ProfilePageData) -> Result<String, String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RssFeedRenderer: Send + Sync {
|
pub trait RssFeedRenderer: Send + Sync {
|
||||||
|
|||||||
@@ -11,3 +11,17 @@ pub struct GetDiaryQuery {
|
|||||||
pub struct GetReviewHistoryQuery {
|
pub struct GetReviewHistoryQuery {
|
||||||
pub movie_id: Uuid,
|
pub movie_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct GetActivityFeedQuery {
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GetUsersQuery;
|
||||||
|
|
||||||
|
pub struct GetUserProfileQuery {
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub view: String,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
}
|
||||||
|
|||||||
13
crates/application/src/use_cases/get_activity_feed.rs
Normal file
13
crates/application/src/use_cases/get_activity_feed.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
models::{FeedEntry, collections::{PageParams, Paginated}},
|
||||||
|
};
|
||||||
|
use crate::{context::AppContext, queries::GetActivityFeedQuery};
|
||||||
|
|
||||||
|
pub async fn execute(
|
||||||
|
ctx: &AppContext,
|
||||||
|
query: GetActivityFeedQuery,
|
||||||
|
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||||
|
let page = PageParams::new(query.limit, query.offset)?;
|
||||||
|
ctx.repository.query_activity_feed(&page).await
|
||||||
|
}
|
||||||
91
crates/application/src/use_cases/get_user_profile.rs
Normal file
91
crates/application/src/use_cases/get_user_profile.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use domain::{
|
||||||
|
errors::DomainError,
|
||||||
|
models::{
|
||||||
|
DiaryEntry, DiaryFilter, MonthActivity, SortDirection, UserStats, UserTrends,
|
||||||
|
collections::{PageParams, Paginated},
|
||||||
|
},
|
||||||
|
value_objects::UserId,
|
||||||
|
};
|
||||||
|
use crate::{context::AppContext, queries::GetUserProfileQuery};
|
||||||
|
|
||||||
|
pub struct UserProfileData {
|
||||||
|
pub stats: UserStats,
|
||||||
|
pub entries: Option<Paginated<DiaryEntry>>,
|
||||||
|
pub history: Option<Vec<MonthActivity>>,
|
||||||
|
pub trends: Option<UserTrends>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(
|
||||||
|
ctx: &AppContext,
|
||||||
|
query: GetUserProfileQuery,
|
||||||
|
) -> Result<UserProfileData, DomainError> {
|
||||||
|
let user_id = UserId::from_uuid(query.user_id);
|
||||||
|
let stats = ctx.repository.get_user_stats(&user_id).await?;
|
||||||
|
|
||||||
|
match query.view.as_str() {
|
||||||
|
"history" => {
|
||||||
|
let all_entries = ctx.repository.get_user_history(&user_id).await?;
|
||||||
|
let history = group_by_month(all_entries);
|
||||||
|
Ok(UserProfileData { stats, entries: None, history: Some(history), trends: None })
|
||||||
|
}
|
||||||
|
"trends" => {
|
||||||
|
let trends = ctx.repository.get_user_trends(&user_id).await?;
|
||||||
|
Ok(UserProfileData { stats, entries: None, history: None, trends: Some(trends) })
|
||||||
|
}
|
||||||
|
"ratings" => {
|
||||||
|
let page = PageParams::new(query.limit, query.offset)?;
|
||||||
|
let filter = DiaryFilter {
|
||||||
|
sort_by: SortDirection::ByRatingDesc,
|
||||||
|
page,
|
||||||
|
movie_id: None,
|
||||||
|
user_id: Some(user_id),
|
||||||
|
};
|
||||||
|
let entries = ctx.repository.query_diary(&filter).await?;
|
||||||
|
Ok(UserProfileData { stats, entries: Some(entries), history: None, trends: None })
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// "recent" (default)
|
||||||
|
let page = PageParams::new(query.limit, query.offset)?;
|
||||||
|
let filter = DiaryFilter {
|
||||||
|
sort_by: SortDirection::Descending,
|
||||||
|
page,
|
||||||
|
movie_id: None,
|
||||||
|
user_id: Some(user_id),
|
||||||
|
};
|
||||||
|
let entries = ctx.repository.query_diary(&filter).await?;
|
||||||
|
Ok(UserProfileData { stats, entries: Some(entries), history: None, trends: None })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn group_by_month(entries: Vec<DiaryEntry>) -> Vec<MonthActivity> {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
let mut map: BTreeMap<String, Vec<DiaryEntry>> = BTreeMap::new();
|
||||||
|
for entry in entries {
|
||||||
|
let ym = entry.review().watched_at().format("%Y-%m").to_string();
|
||||||
|
map.entry(ym).or_default().push(entry);
|
||||||
|
}
|
||||||
|
let mut result: Vec<MonthActivity> = map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(ym, entries)| MonthActivity {
|
||||||
|
month_label: format_year_month_long(&ym),
|
||||||
|
count: entries.len() as i64,
|
||||||
|
entries,
|
||||||
|
year_month: ym,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
result.reverse();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_year_month_long(ym: &str) -> String {
|
||||||
|
let parts: Vec<&str> = ym.splitn(2, '-').collect();
|
||||||
|
if parts.len() != 2 { return ym.to_string(); }
|
||||||
|
let month = match parts[1] {
|
||||||
|
"01" => "January", "02" => "February", "03" => "March", "04" => "April",
|
||||||
|
"05" => "May", "06" => "June", "07" => "July", "08" => "August",
|
||||||
|
"09" => "September", "10" => "October", "11" => "November", "12" => "December",
|
||||||
|
_ => parts[1],
|
||||||
|
};
|
||||||
|
format!("{} {}", month, parts[0])
|
||||||
|
}
|
||||||
9
crates/application/src/use_cases/get_users.rs
Normal file
9
crates/application/src/use_cases/get_users.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use domain::{errors::DomainError, models::UserSummary};
|
||||||
|
use crate::{context::AppContext, queries::GetUsersQuery};
|
||||||
|
|
||||||
|
pub async fn execute(
|
||||||
|
ctx: &AppContext,
|
||||||
|
_query: GetUsersQuery,
|
||||||
|
) -> Result<Vec<UserSummary>, DomainError> {
|
||||||
|
ctx.user_repository.list_with_stats().await
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
pub mod delete_review;
|
pub mod delete_review;
|
||||||
|
pub mod get_activity_feed;
|
||||||
pub mod get_diary;
|
pub mod get_diary;
|
||||||
pub mod get_review_history;
|
pub mod get_review_history;
|
||||||
|
pub mod get_user_profile;
|
||||||
|
pub mod get_users;
|
||||||
pub mod log_review;
|
pub mod log_review;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
|
|||||||
Reference in New Issue
Block a user