feat: ux improvements
This commit is contained in:
@@ -85,8 +85,8 @@ impl SqliteMovieRepository {
|
|||||||
offset: i64,
|
offset: i64,
|
||||||
) -> Result<Vec<DiaryRow>, DomainError> {
|
) -> Result<Vec<DiaryRow>, DomainError> {
|
||||||
match sort {
|
match sort {
|
||||||
// ByRatingDesc only applies to user-scoped queries; falls back to date sort here
|
// ByRatingDesc/ByRatingAsc only apply to user-scoped queries; fall back to date sort here
|
||||||
SortDirection::Descending | SortDirection::ByRatingDesc => sqlx::query_as!(
|
SortDirection::Descending | SortDirection::ByRatingDesc | SortDirection::ByRatingAsc => sqlx::query_as!(
|
||||||
DiaryRow,
|
DiaryRow,
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url
|
r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url
|
||||||
@@ -126,8 +126,8 @@ impl SqliteMovieRepository {
|
|||||||
offset: i64,
|
offset: i64,
|
||||||
) -> Result<Vec<DiaryRow>, DomainError> {
|
) -> Result<Vec<DiaryRow>, DomainError> {
|
||||||
match sort {
|
match sort {
|
||||||
// ByRatingDesc only applies to user-scoped queries; falls back to date sort here
|
// ByRatingDesc/ByRatingAsc only apply to user-scoped queries; fall back to date sort here
|
||||||
SortDirection::Descending | SortDirection::ByRatingDesc => sqlx::query_as!(
|
SortDirection::Descending | SortDirection::ByRatingDesc | SortDirection::ByRatingAsc => sqlx::query_as!(
|
||||||
DiaryRow,
|
DiaryRow,
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url
|
r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url
|
||||||
@@ -163,57 +163,71 @@ impl SqliteMovieRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn count_user_diary_entries(&self, user_id: &str) -> Result<i64, DomainError> {
|
async fn count_user_diary_entries(
|
||||||
sqlx::query_scalar!("SELECT COUNT(*) FROM reviews WHERE user_id = ?", user_id)
|
&self,
|
||||||
.fetch_one(&self.pool)
|
user_id: &str,
|
||||||
|
search: Option<&str>,
|
||||||
|
) -> Result<i64, DomainError> {
|
||||||
|
let has_search = search.map(|s| !s.is_empty()).unwrap_or(false);
|
||||||
|
let sql = if has_search {
|
||||||
|
"SELECT COUNT(*) FROM reviews r
|
||||||
|
INNER JOIN movies m ON m.id = r.movie_id
|
||||||
|
WHERE r.user_id = ? AND m.title LIKE '%' || ? || '%'"
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
"SELECT COUNT(*) FROM reviews r
|
||||||
|
INNER JOIN movies m ON m.id = r.movie_id
|
||||||
|
WHERE r.user_id = ?"
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
let mut q = sqlx::query_scalar::<_, i64>(&sql).bind(user_id);
|
||||||
|
if has_search {
|
||||||
|
q = q.bind(search.unwrap());
|
||||||
|
}
|
||||||
|
q.fetch_one(&self.pool).await.map_err(Self::map_err)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_user_diary_rows(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
sort: &SortDirection,
|
||||||
|
search: Option<&str>,
|
||||||
|
limit: i64,
|
||||||
|
offset: i64,
|
||||||
|
) -> Result<Vec<DiaryRow>, DomainError> {
|
||||||
|
let has_search = search.map(|s| !s.is_empty()).unwrap_or(false);
|
||||||
|
let search_clause = if has_search {
|
||||||
|
" AND m.title LIKE '%' || ? || '%'"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let order_clause = match sort {
|
||||||
|
SortDirection::ByRatingDesc => "r.rating DESC, r.watched_at DESC",
|
||||||
|
SortDirection::ByRatingAsc => "r.rating ASC, r.watched_at ASC",
|
||||||
|
SortDirection::Ascending => "r.watched_at ASC",
|
||||||
|
SortDirection::Descending => "r.watched_at DESC",
|
||||||
|
};
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
||||||
|
r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url
|
||||||
|
FROM reviews r
|
||||||
|
INNER JOIN movies m ON m.id = r.movie_id
|
||||||
|
WHERE r.user_id = ?{}
|
||||||
|
ORDER BY {}
|
||||||
|
LIMIT ? OFFSET ?",
|
||||||
|
search_clause, order_clause
|
||||||
|
);
|
||||||
|
let mut q = sqlx::query_as::<_, DiaryRow>(&sql).bind(user_id);
|
||||||
|
if has_search {
|
||||||
|
q = q.bind(search.unwrap());
|
||||||
|
}
|
||||||
|
q.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_err)
|
.map_err(Self::map_err)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_user_diary_rows_by_watched(
|
|
||||||
&self,
|
|
||||||
user_id: &str,
|
|
||||||
limit: i64,
|
|
||||||
offset: i64,
|
|
||||||
) -> Result<Vec<DiaryRow>, DomainError> {
|
|
||||||
sqlx::query_as!(
|
|
||||||
DiaryRow,
|
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
|
||||||
r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url
|
|
||||||
FROM reviews r
|
|
||||||
INNER JOIN movies m ON m.id = r.movie_id
|
|
||||||
WHERE r.user_id = ?
|
|
||||||
ORDER BY r.watched_at DESC
|
|
||||||
LIMIT ? OFFSET ?",
|
|
||||||
user_id, limit, offset
|
|
||||||
)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_err)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_user_diary_rows_by_rating(
|
|
||||||
&self,
|
|
||||||
user_id: &str,
|
|
||||||
limit: i64,
|
|
||||||
offset: i64,
|
|
||||||
) -> Result<Vec<DiaryRow>, DomainError> {
|
|
||||||
sqlx::query_as!(
|
|
||||||
DiaryRow,
|
|
||||||
"SELECT m.id, m.external_metadata_id, m.title, m.release_year, m.director, m.poster_path,
|
|
||||||
r.id AS review_id, r.movie_id, r.user_id, r.rating, r.comment, r.watched_at, r.created_at, r.remote_actor_url
|
|
||||||
FROM reviews r
|
|
||||||
INNER JOIN movies m ON m.id = r.movie_id
|
|
||||||
WHERE r.user_id = ?
|
|
||||||
ORDER BY r.rating DESC, r.watched_at DESC
|
|
||||||
LIMIT ? OFFSET ?",
|
|
||||||
user_id, limit, offset
|
|
||||||
)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Self::map_err)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_user_totals(&self, user_id: &str) -> Result<UserTotalsRow, DomainError> {
|
async fn fetch_user_totals(&self, user_id: &str) -> Result<UserTotalsRow, DomainError> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
UserTotalsRow,
|
UserTotalsRow,
|
||||||
@@ -460,16 +474,11 @@ impl DiaryRepository for SqliteMovieRepository {
|
|||||||
}
|
}
|
||||||
(None, Some(uid)) => {
|
(None, Some(uid)) => {
|
||||||
let uid_str = uid.value().to_string();
|
let uid_str = uid.value().to_string();
|
||||||
match &filter.sort_by {
|
let search = filter.search.as_deref();
|
||||||
SortDirection::ByRatingDesc => tokio::try_join!(
|
tokio::try_join!(
|
||||||
self.count_user_diary_entries(&uid_str),
|
self.count_user_diary_entries(&uid_str, search),
|
||||||
self.fetch_user_diary_rows_by_rating(&uid_str, limit, offset)
|
self.fetch_user_diary_rows(&uid_str, &filter.sort_by, search, limit, offset)
|
||||||
)?,
|
)?
|
||||||
_ => tokio::try_join!(
|
|
||||||
self.count_user_diary_entries(&uid_str),
|
|
||||||
self.fetch_user_diary_rows_by_watched(&uid_str, limit, offset)
|
|
||||||
)?,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(Some(_), Some(_)) => {
|
(Some(_), Some(_)) => {
|
||||||
return Err(DomainError::ValidationError(
|
return Err(DomainError::ValidationError(
|
||||||
|
|||||||
@@ -163,6 +163,26 @@ struct ProfileTemplate<'a> {
|
|||||||
following_count: usize,
|
following_count: usize,
|
||||||
followers_count: usize,
|
followers_count: usize,
|
||||||
pending_followers: Vec<RemoteActorData>,
|
pending_followers: Vec<RemoteActorData>,
|
||||||
|
pub sort_by: String,
|
||||||
|
pub search: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ProfileTemplate<'a> {
|
||||||
|
pub fn filter_qs(&self) -> String {
|
||||||
|
let mut parts = vec![
|
||||||
|
format!("view={}", self.view),
|
||||||
|
format!("sort_by={}", self.sort_by),
|
||||||
|
];
|
||||||
|
if !self.search.is_empty() {
|
||||||
|
let encoded = self.search
|
||||||
|
.replace(' ', "+")
|
||||||
|
.replace('#', "%23")
|
||||||
|
.replace('&', "%26")
|
||||||
|
.replace('=', "%3D");
|
||||||
|
parts.push(format!("search={}", encoded));
|
||||||
|
}
|
||||||
|
format!("&{}", parts.join("&"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RemoteActorData {
|
struct RemoteActorData {
|
||||||
@@ -493,6 +513,8 @@ impl HtmlRenderer for AskamaHtmlRenderer {
|
|||||||
display_name: a.display_name,
|
display_name: a.display_name,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
sort_by: data.sort_by.clone(),
|
||||||
|
search: data.search.clone(),
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="feed-controls">
|
||||||
|
<select name="sort_by" onchange="this.form.submit()">
|
||||||
|
<option value="date"{% if sort_by == "date" %} selected{% endif %}>Date: newest first</option>
|
||||||
|
<option value="date_asc"{% if sort_by == "date_asc" %} selected{% endif %}>Date: oldest first</option>
|
||||||
|
<option value="rating"{% if sort_by == "rating" %} selected{% endif %}>Rating: highest first</option>
|
||||||
|
<option value="rating_asc"{% if sort_by == "rating_asc" %} selected{% endif %}>Rating: lowest first</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="search" value="{{ search }}" placeholder="Search movies...">
|
||||||
|
<button type="submit" class="btn-search">Search</button>
|
||||||
|
</div>
|
||||||
@@ -15,19 +15,10 @@
|
|||||||
Following
|
Following
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="feed-controls">
|
{% include "_filter_controls.html" %}
|
||||||
<select name="sort_by" onchange="this.form.submit()">
|
{% if filter != "all" || sort_by != "date" || !search.is_empty() %}
|
||||||
<option value="date"{% if sort_by == "date" %} selected{% endif %}>Date: newest first</option>
|
<a href="/" class="clear-filters">Clear</a>
|
||||||
<option value="date_asc"{% if sort_by == "date_asc" %} selected{% endif %}>Date: oldest first</option>
|
{% endif %}
|
||||||
<option value="rating"{% if sort_by == "rating" %} selected{% endif %}>Rating: highest first</option>
|
|
||||||
<option value="rating_asc"{% if sort_by == "rating_asc" %} selected{% endif %}>Rating: lowest first</option>
|
|
||||||
</select>
|
|
||||||
<input type="text" name="search" value="{{ search }}" placeholder="Search movies...">
|
|
||||||
<button type="submit" class="btn-search">Search</button>
|
|
||||||
{% if filter != "all" || sort_by != "date" || !search.is_empty() %}
|
|
||||||
<a href="/" class="clear-filters">Clear</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="limit" value="{{ limit }}">
|
<input type="hidden" name="limit" value="{{ limit }}">
|
||||||
</form>
|
</form>
|
||||||
<div class="diary">
|
<div class="diary">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
{% if let Some(name) = actor.display_name %}
|
{% if let Some(name) = actor.display_name %}
|
||||||
({{ name }})
|
({{ name }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
|
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">View profile ↗</a>
|
||||||
<form method="POST" action="/users/{{ user_id }}/followers/remove" style="display:inline">
|
<form method="POST" action="/users/{{ user_id }}/followers/remove" style="display:inline">
|
||||||
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
||||||
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
{% if let Some(name) = actor.display_name %}
|
{% if let Some(name) = actor.display_name %}
|
||||||
({{ name }})
|
({{ name }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
|
<a href="{{ actor.url }}" target="_blank" rel="noopener noreferrer">View profile ↗</a>
|
||||||
<form method="POST" action="/users/{{ user_id }}/unfollow" style="display:inline">
|
<form method="POST" action="/users/{{ user_id }}/unfollow" style="display:inline">
|
||||||
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
||||||
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
|
|||||||
@@ -75,6 +75,17 @@
|
|||||||
<a href="?view=trends" class="view-tab {% if view == "trends" %}active{% endif %}">Trends</a>
|
<a href="?view=trends" class="view-tab {% if view == "trends" %}active{% endif %}">Trends</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if view == "recent" || view == "ratings" %}
|
||||||
|
<form method="get" class="feed-filters" action="/users/{{ profile_user_id }}">
|
||||||
|
<input type="hidden" name="view" value="{{ view }}">
|
||||||
|
<input type="hidden" name="limit" value="{{ limit }}">
|
||||||
|
{% include "_filter_controls.html" %}
|
||||||
|
{% if sort_by != "date" || !search.is_empty() %}
|
||||||
|
<a href="/users/{{ profile_user_id }}?view={{ view }}" class="clear-filters">Clear</a>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if view == "history" %}
|
{% if view == "history" %}
|
||||||
{% if let Some(hist) = history %}
|
{% if let Some(hist) = history %}
|
||||||
<div class="heatmap-section">
|
<div class="heatmap-section">
|
||||||
@@ -185,7 +196,7 @@
|
|||||||
<div class="watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
|
<div class="watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
|
||||||
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
||||||
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
||||||
<input type="hidden" name="redirect_after" value="/users/{{ profile_user_id }}?view={{ view }}&offset={{ current_offset }}">
|
<input type="hidden" name="redirect_after" value="/users/{{ profile_user_id }}?offset={{ current_offset }}{{ self.filter_qs() }}">
|
||||||
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
<input type="hidden" name="_csrf" value="{{ ctx.csrf_token }}">
|
||||||
<button type="submit">Delete</button>
|
<button type="submit">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -198,7 +209,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav class="pagination">
|
<nav class="pagination">
|
||||||
{% if current_offset >= limit %}
|
{% if current_offset >= limit %}
|
||||||
<a href="?view={{ view }}&offset={{ current_offset - limit }}" class="page-nav">← Prev</a>
|
<a href="/users/{{ profile_user_id }}?offset={{ current_offset - limit }}{{ self.filter_qs() }}" class="page-nav">← Prev</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for item in page_items %}
|
{% for item in page_items %}
|
||||||
{% if item.is_ellipsis %}
|
{% if item.is_ellipsis %}
|
||||||
@@ -206,11 +217,11 @@
|
|||||||
{% elif item.is_current %}
|
{% elif item.is_current %}
|
||||||
<span class="page-num current">{{ item.number + 1 }}</span>
|
<span class="page-num current">{{ item.number + 1 }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="?view={{ view }}&offset={{ item.number * limit }}" class="page-num">{{ item.number + 1 }}</a>
|
<a href="/users/{{ profile_user_id }}?offset={{ item.number * limit }}{{ self.filter_qs() }}" class="page-num">{{ item.number + 1 }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if has_more %}
|
{% if has_more %}
|
||||||
<a href="?view={{ view }}&offset={{ current_offset + limit }}" class="page-nav">Next →</a>
|
<a href="/users/{{ profile_user_id }}?offset={{ current_offset + limit }}{{ self.filter_qs() }}" class="page-nav">Next →</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ pub struct ProfilePageData {
|
|||||||
pub following_count: usize,
|
pub following_count: usize,
|
||||||
pub followers_count: usize,
|
pub followers_count: usize,
|
||||||
pub pending_followers: Vec<RemoteActorView>,
|
pub pending_followers: Vec<RemoteActorView>,
|
||||||
|
pub sort_by: String,
|
||||||
|
pub search: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FollowingPageData {
|
pub struct FollowingPageData {
|
||||||
|
|||||||
@@ -61,4 +61,6 @@ pub struct GetUserProfileQuery {
|
|||||||
pub view: ProfileView,
|
pub view: ProfileView,
|
||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
pub offset: Option<u32>,
|
pub offset: Option<u32>,
|
||||||
|
pub sort_by: domain::ports::FeedSortBy,
|
||||||
|
pub search: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub async fn execute(
|
|||||||
page,
|
page,
|
||||||
movie_id,
|
movie_id,
|
||||||
user_id,
|
user_id,
|
||||||
|
search: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.diary_repository.query_diary(&filter).await
|
ctx.diary_repository.query_diary(&filter).await
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use domain::{
|
|||||||
DiaryEntry, DiaryFilter, MonthActivity, SortDirection, UserStats, UserTrends,
|
DiaryEntry, DiaryFilter, MonthActivity, SortDirection, UserStats, UserTrends,
|
||||||
collections::{PageParams, Paginated},
|
collections::{PageParams, Paginated},
|
||||||
},
|
},
|
||||||
|
ports::FeedSortBy,
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,11 +48,13 @@ pub async fn execute(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
ProfileView::Ratings => {
|
ProfileView::Ratings => {
|
||||||
|
let sort_direction = feed_sort_to_direction(query.sort_by);
|
||||||
let filter = paged_user_filter(
|
let filter = paged_user_filter(
|
||||||
user_id,
|
user_id,
|
||||||
SortDirection::ByRatingDesc,
|
sort_direction,
|
||||||
query.limit,
|
query.limit,
|
||||||
query.offset,
|
query.offset,
|
||||||
|
query.search.clone(),
|
||||||
)?;
|
)?;
|
||||||
let entries = ctx.diary_repository.query_diary(&filter).await?;
|
let entries = ctx.diary_repository.query_diary(&filter).await?;
|
||||||
Ok(UserProfileData {
|
Ok(UserProfileData {
|
||||||
@@ -62,11 +65,13 @@ pub async fn execute(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
ProfileView::Recent => {
|
ProfileView::Recent => {
|
||||||
|
let sort_direction = feed_sort_to_direction(query.sort_by);
|
||||||
let filter = paged_user_filter(
|
let filter = paged_user_filter(
|
||||||
user_id,
|
user_id,
|
||||||
SortDirection::Descending,
|
sort_direction,
|
||||||
query.limit,
|
query.limit,
|
||||||
query.offset,
|
query.offset,
|
||||||
|
query.search.clone(),
|
||||||
)?;
|
)?;
|
||||||
let entries = ctx.diary_repository.query_diary(&filter).await?;
|
let entries = ctx.diary_repository.query_diary(&filter).await?;
|
||||||
Ok(UserProfileData {
|
Ok(UserProfileData {
|
||||||
@@ -79,11 +84,21 @@ pub async fn execute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn feed_sort_to_direction(sort_by: FeedSortBy) -> SortDirection {
|
||||||
|
match sort_by {
|
||||||
|
FeedSortBy::Date => SortDirection::Descending,
|
||||||
|
FeedSortBy::DateAsc => SortDirection::Ascending,
|
||||||
|
FeedSortBy::Rating => SortDirection::ByRatingDesc,
|
||||||
|
FeedSortBy::RatingAsc => SortDirection::ByRatingAsc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn paged_user_filter(
|
fn paged_user_filter(
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
sort_by: SortDirection,
|
sort_by: SortDirection,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
|
search: Option<String>,
|
||||||
) -> Result<DiaryFilter, DomainError> {
|
) -> Result<DiaryFilter, DomainError> {
|
||||||
let page = PageParams::new(limit, offset)?;
|
let page = PageParams::new(limit, offset)?;
|
||||||
Ok(DiaryFilter {
|
Ok(DiaryFilter {
|
||||||
@@ -91,6 +106,7 @@ fn paged_user_filter(
|
|||||||
page,
|
page,
|
||||||
movie_id: None,
|
movie_id: None,
|
||||||
user_id: Some(user_id),
|
user_id: Some(user_id),
|
||||||
|
search,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub enum SortDirection {
|
|||||||
Descending,
|
Descending,
|
||||||
Ascending,
|
Ascending,
|
||||||
ByRatingDesc,
|
ByRatingDesc,
|
||||||
|
ByRatingAsc,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
@@ -24,6 +25,7 @@ pub struct DiaryFilter {
|
|||||||
pub page: PageParams,
|
pub page: PageParams,
|
||||||
pub movie_id: Option<MovieId>,
|
pub movie_id: Option<MovieId>,
|
||||||
pub user_id: Option<UserId>,
|
pub user_id: Option<UserId>,
|
||||||
|
pub search: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -284,6 +284,10 @@ pub struct ProfileQueryParams {
|
|||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
pub offset: Option<u32>,
|
pub offset: Option<u32>,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sort_by: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub search: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Activity feed ─────────────────────────────────────────────────────────────
|
// ── Activity feed ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -529,6 +529,13 @@ pub mod html {
|
|||||||
state.app_ctx.config.base_url, profile_user_uuid
|
state.app_ctx.config.base_url, profile_user_uuid
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let sort_by_str = match params.sort_by.as_str() {
|
||||||
|
"date_asc" => "date_asc",
|
||||||
|
"rating" => "rating",
|
||||||
|
"rating_asc" => "rating_asc",
|
||||||
|
_ => "date",
|
||||||
|
};
|
||||||
|
|
||||||
let is_own_profile = user_id
|
let is_own_profile = user_id
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|u| u.value() == profile_user_uuid)
|
.map(|u| u.value() == profile_user_uuid)
|
||||||
@@ -580,6 +587,8 @@ pub mod html {
|
|||||||
view: profile_view,
|
view: profile_view,
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
offset: params.offset,
|
offset: params.offset,
|
||||||
|
sort_by: domain::ports::FeedSortBy::from_str(sort_by_str),
|
||||||
|
search: if params.search.is_empty() { None } else { Some(params.search.clone()) },
|
||||||
};
|
};
|
||||||
|
|
||||||
match application::use_cases::get_user_profile::execute(&state.app_ctx, query).await {
|
match application::use_cases::get_user_profile::execute(&state.app_ctx, query).await {
|
||||||
@@ -611,6 +620,8 @@ pub mod html {
|
|||||||
following_count,
|
following_count,
|
||||||
followers_count,
|
followers_count,
|
||||||
pending_followers,
|
pending_followers,
|
||||||
|
sort_by: sort_by_str.to_string(),
|
||||||
|
search: params.search.clone(),
|
||||||
};
|
};
|
||||||
match state.html_renderer.render_profile_page(data) {
|
match state.html_renderer.render_profile_page(data) {
|
||||||
Ok(html) => Html(html).into_response(),
|
Ok(html) => Html(html).into_response(),
|
||||||
@@ -1517,6 +1528,8 @@ pub mod api {
|
|||||||
view: profile_view,
|
view: profile_view,
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
offset: params.offset,
|
offset: params.offset,
|
||||||
|
sort_by: domain::ports::FeedSortBy::Date,
|
||||||
|
search: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ form button[type="submit"]:hover {
|
|||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user