From ef5576ab3f402d710111a960855d92b695e0cefc Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Mon, 4 Nov 2024 17:15:55 -0500 Subject: [PATCH] Create profile page --- src/app.rs | 5 +- src/artistdata.rs | 2 +- src/pages.rs | 1 + src/pages/profile.rs | 330 +++++++++++++++++++++++++++++++++++++++++++ style/main.scss | 1 + style/profile.scss | 36 +++++ 6 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 src/pages/profile.rs create mode 100644 style/profile.scss diff --git a/src/app.rs b/src/app.rs index 50cad40..15d877f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,7 @@ use leptos_meta::*; use leptos_router::*; use crate::pages::login::*; use crate::pages::signup::*; +use crate::pages::profile::*; use crate::error_template::{AppError, ErrorTemplate}; use crate::auth::get_logged_in_user; use crate::models::User; @@ -58,6 +59,8 @@ pub fn App() -> impl IntoView { + } /> + } /> } /> } /> @@ -70,7 +73,7 @@ pub fn App() -> impl IntoView { use crate::components::sidebar::*; use crate::components::dashboard::*; use crate::components::search::*; -use crate::components::personal::*; +use crate::components::personal::Personal; use crate::components::upload::*; /// Renders the home page of your application. diff --git a/src/artistdata.rs b/src/artistdata.rs index 9a2d5f2..401979d 100644 --- a/src/artistdata.rs +++ b/src/artistdata.rs @@ -4,7 +4,7 @@ use serde::{Serialize, Deserialize}; /// Holds information about an artist /// /// Intended to be used in the front-end -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct ArtistData { /// Artist id pub id: i32, diff --git a/src/pages.rs b/src/pages.rs index 815d2a9..1e65f58 100644 --- a/src/pages.rs +++ b/src/pages.rs @@ -1,2 +1,3 @@ pub mod login; pub mod signup; +pub mod profile; diff --git a/src/pages/profile.rs b/src/pages/profile.rs new file mode 100644 index 0000000..8bec37a --- /dev/null +++ b/src/pages/profile.rs @@ -0,0 +1,330 @@ +use leptos::*; +use leptos::logging::*; +use leptos_router::use_params_map; +use leptos_icons::*; +use server_fn::error::NoCustomError; + +use crate::components::dashboard_row::DashboardRow; +use crate::components::dashboard_tile::DashboardTile; +use crate::components::song_list::*; +use crate::components::loading::*; +use crate::components::error::*; + +use crate::api::profile::*; + +use crate::app::LoggedInUserResource; +use crate::models::User; +use crate::users::get_user_by_id; + +/// Duration in seconds backwards from now to aggregate history data for +const HISTORY_SECS: u64 = 60 * 60 * 24 * 30; +const HISTORY_MESSAGE: &str = "Last Month"; + +/// How many top songs to show +const TOP_SONGS_COUNT: i64 = 10; +/// How many recent songs to show +const RECENT_SONGS_COUNT: i64 = 5; +/// How many recent artists to show +const TOP_ARTISTS_COUNT: i64 = 10; + +/// Profile page +/// Shows the current user's profile if no id is specified, or a user's profile if an id is specified in the path +#[component] +pub fn Profile(logged_in_user: LoggedInUserResource) -> impl IntoView { + let params = use_params_map(); + + view! { +
+ {move || params.with(|params| { + match params.get("id").map(|id| id.parse::()) { + None => { + // No id specified, show the current user's profile + view! { }.into_view() + }, + Some(Ok(id)) => { + // Id specified, get the user and show their profile + view! { }.into_view() + }, + Some(Err(e)) => { + // Invalid id, return an error + view! { + + title="Invalid User ID" + error=e.to_string() + /> + }.into_view() + } + } + })} +
+ } +} + +/// Show the logged in user's profile +#[component] +fn OwnProfile(logged_in_user: LoggedInUserResource) -> impl IntoView { + view! { + } + > + {move || logged_in_user.get().map(|user| { + match user { + Some(user) => { + let user_id = user.id.unwrap(); + view! { + + + + + }.into_view() + }, + None => view! { + + title="Not Logged In" + message="You must be logged in to view your profile" + /> + }.into_view(), + } + })} + + } +} + +/// Show a user's profile by ID +#[component] +fn UserIdProfile(#[prop(into)] id: MaybeSignal) -> impl IntoView { + let user_info = create_resource(move || id.get(), move |id| { + get_user_by_id(id) + }); + + // Show the details if the user is found + let show_details = create_rw_signal(false); + + view!{ + } + > + {move || user_info.get().map(|user| { + match user { + Ok(Some(user)) => { + show_details.set(true); + + view! { }.into_view() + }, + Ok(None) => { + show_details.set(false); + + view! { + + title="User Not Found" + message=format!("User with ID {} not found", id.get()) + /> + }.into_view() + }, + Err(error) => { + show_details.set(false); + + view! { + + title="Error Getting User" + error + /> + }.into_view() + } + } + })} + + + } +} + +/// Show a profile for a User object +#[component] +fn UserProfile(user: User) -> impl IntoView { + let user_id = user.id.unwrap(); + let profile_image_path = format!("/assets/images/profile/{}.webp", user_id); + + view! { +
+ + + +

{user.username}

+
+
+

+ {user.email} + { + user.created_at.map(|created_at| { + use time::{OffsetDateTime, macros::format_description}; + let format = format_description!("[month repr:long] [year]"); + let date_time = Into::::into(created_at).format(format); + + match date_time { + Ok(date_time) => { + format!(" • Joined {}", date_time) + }, + Err(e) => { + error!("Error formatting date: {}", e); + String::new() + } + } + }) + } + { + if user.admin { + " • Admin" + } else { + "" + } + } +

+
+ } +} + +/// Show a list of top songs for a user +#[component] +fn TopSongs(#[prop(into)] user_id: MaybeSignal) -> impl IntoView { + let top_songs = create_resource(move || user_id.get(), |user_id| async move { + use std::time::{SystemTime, Duration}; + + let now = SystemTime::now(); + let start = now - Duration::from_secs(HISTORY_SECS); + let top_songs = top_songs(user_id, start, now, Some(TOP_SONGS_COUNT)).await; + + top_songs.map(|top_songs| { + top_songs.into_iter().map(|(plays, song)| { + let plays = if plays == 1 { + format!("{} Play", plays) + } else { + format!("{} Plays", plays) + }; + + (song, plays) + }).collect::>() + }) + }); + + view! { +

{format!("Top Songs {}", HISTORY_MESSAGE)}

+ } + > + {e.to_string()}

}) + .collect_view() + } + } + > + {move || + top_songs.get().map(|top_songs| { + top_songs.map(|top_songs| { + view! { + + } + }) + }) + } +
+
+ } +} + +/// Show a list of recently played songs for a user +#[component] +fn RecentSongs(#[prop(into)] user_id: MaybeSignal) -> impl IntoView { + let recent_songs = create_resource(move || user_id.get(), |user_id| async move { + let recent_songs = recent_songs(user_id, Some(RECENT_SONGS_COUNT)).await; + + recent_songs.map(|recent_songs| { + recent_songs.into_iter().map(|(_date, song)| { + song + }).collect::>() + }) + }); + + view! { +

"Recently Played"

+ } + > + {e.to_string()}

}) + .collect_view() + } + } + > + {move || + recent_songs.get().map(|recent_songs| { + recent_songs.map(|recent_songs| { + view! { + + } + }) + }) + } +
+
+ } +} + +/// Show a list of top artists for a user +#[component] +fn TopArtists(#[prop(into)] user_id: MaybeSignal) -> impl IntoView { + let top_artists = create_resource(move || user_id.get(), |user_id| async move { + use std::time::{SystemTime, Duration}; + + let now = SystemTime::now(); + let start = now - Duration::from_secs(HISTORY_SECS); + let top_artists = top_artists(user_id, start, now, Some(TOP_ARTISTS_COUNT)).await; + + top_artists.map(|top_artists| { + top_artists.into_iter().map(|(_plays, artist)| { + artist + }).collect::>() + }) + }); + + view! { + {format!("Top Artists {}", HISTORY_MESSAGE)} + + } + > + {format!("Top Artists {}", HISTORY_MESSAGE)} + {move || errors.get() + .into_iter() + .map(|(_, e)| view! {

{e.to_string()}

}) + .collect_view() + } + } + > + {move || + top_artists.get().map(|top_artists| { + top_artists.map(|top_artists| { + let tiles = top_artists.into_iter().map(|artist| { + Box::new(artist) as Box + }).collect::>(); + + DashboardRow::new(format!("Top Artists {}", HISTORY_MESSAGE), tiles) + }) + }) + } +
+
+ } +} diff --git a/style/main.scss b/style/main.scss index c581d58..a19abe7 100644 --- a/style/main.scss +++ b/style/main.scss @@ -13,6 +13,7 @@ @import 'upload.scss'; @import 'error.scss'; @import 'song_list.scss'; +@import 'profile.scss'; @import 'loading.scss'; body { diff --git a/style/profile.scss b/style/profile.scss new file mode 100644 index 0000000..e76c09a --- /dev/null +++ b/style/profile.scss @@ -0,0 +1,36 @@ +@import 'theme.scss'; + +.profile-container { + .profile-header { + display: flex; + + .profile-image { + width: 75px; + height: 75px; + border-radius: 50%; + padding: 10px; + padding-bottom: 5px; + margin-top: auto; + margin-bottom: auto; + + svg { + padding: 0; + margin: 0; + } + } + + h1 { + font-size: 40px; + align-self: center; + padding: 10px; + padding-bottom: 5px; + } + } + + .profile-details { + p { + font-size: 1rem; + margin: 0.5rem; + } + } +}