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.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;
+ }
+ }
+}