diff --git a/crates/application/src/ports.rs b/crates/application/src/ports.rs index 1b03f17..52a8604 100644 --- a/crates/application/src/ports.rs +++ b/crates/application/src/ports.rs @@ -1,6 +1,6 @@ use uuid::Uuid; -use domain::models::{DiaryEntry, collections::Paginated}; +use domain::models::{DiaryEntry, FeedEntry, MonthActivity, UserStats, UserSummary, UserTrends, collections::Paginated}; pub struct HtmlPageContext { pub user_email: Option, @@ -23,11 +23,41 @@ pub struct NewReviewPageData<'a> { pub error: Option<&'a str>, } +pub struct ActivityFeedPageData { + pub ctx: HtmlPageContext, + pub entries: Paginated, + pub current_offset: u32, + pub has_more: bool, + pub limit: u32, +} + +pub struct UsersPageData { + pub ctx: HtmlPageContext, + pub users: Vec, +} + +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>, + pub current_offset: u32, + pub has_more: bool, + pub limit: u32, + pub history: Option>, + pub trends: Option, +} + pub trait HtmlRenderer: Send + Sync { fn render_diary_page(&self, data: &Paginated, ctx: HtmlPageContext) -> Result; fn render_login_page(&self, data: LoginPageData<'_>) -> Result; fn render_register_page(&self, data: RegisterPageData<'_>) -> Result; fn render_new_review_page(&self, data: NewReviewPageData<'_>) -> Result; + fn render_activity_feed_page(&self, data: ActivityFeedPageData) -> Result; + fn render_users_page(&self, data: UsersPageData) -> Result; + fn render_profile_page(&self, data: ProfilePageData) -> Result; } pub trait RssFeedRenderer: Send + Sync { diff --git a/crates/application/src/queries.rs b/crates/application/src/queries.rs index 8619b05..b4898f0 100644 --- a/crates/application/src/queries.rs +++ b/crates/application/src/queries.rs @@ -11,3 +11,17 @@ pub struct GetDiaryQuery { pub struct GetReviewHistoryQuery { pub movie_id: Uuid, } + +pub struct GetActivityFeedQuery { + pub limit: Option, + pub offset: Option, +} + +pub struct GetUsersQuery; + +pub struct GetUserProfileQuery { + pub user_id: Uuid, + pub view: String, + pub limit: Option, + pub offset: Option, +} diff --git a/crates/application/src/use_cases/get_activity_feed.rs b/crates/application/src/use_cases/get_activity_feed.rs new file mode 100644 index 0000000..afee970 --- /dev/null +++ b/crates/application/src/use_cases/get_activity_feed.rs @@ -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, DomainError> { + let page = PageParams::new(query.limit, query.offset)?; + ctx.repository.query_activity_feed(&page).await +} diff --git a/crates/application/src/use_cases/get_user_profile.rs b/crates/application/src/use_cases/get_user_profile.rs new file mode 100644 index 0000000..9ad587d --- /dev/null +++ b/crates/application/src/use_cases/get_user_profile.rs @@ -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>, + pub history: Option>, + pub trends: Option, +} + +pub async fn execute( + ctx: &AppContext, + query: GetUserProfileQuery, +) -> Result { + 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) -> Vec { + use std::collections::BTreeMap; + let mut map: BTreeMap> = 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 = 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]) +} diff --git a/crates/application/src/use_cases/get_users.rs b/crates/application/src/use_cases/get_users.rs new file mode 100644 index 0000000..2efd6e4 --- /dev/null +++ b/crates/application/src/use_cases/get_users.rs @@ -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, DomainError> { + ctx.user_repository.list_with_stats().await +} diff --git a/crates/application/src/use_cases/mod.rs b/crates/application/src/use_cases/mod.rs index 63da43b..8f8bdb7 100644 --- a/crates/application/src/use_cases/mod.rs +++ b/crates/application/src/use_cases/mod.rs @@ -1,6 +1,9 @@ pub mod delete_review; +pub mod get_activity_feed; pub mod get_diary; pub mod get_review_history; +pub mod get_user_profile; +pub mod get_users; pub mod log_review; pub mod login; pub mod register;