diff --git a/frontend/css/sass/stats.scss b/frontend/css/sass/stats.scss index 338578f3ca..e38d8649b5 100644 --- a/frontend/css/sass/stats.scss +++ b/frontend/css/sass/stats.scss @@ -1,8 +1,8 @@ @use "sass:map"; .user-stats-card { - margin-top: 20px; padding: 0 20px; + min-height: 400px; } .stats-full-width-graph { diff --git a/frontend/js/src/user/stats/UserReports.tsx b/frontend/js/src/user/stats/UserReports.tsx index c50192e0b4..fd0895244e 100644 --- a/frontend/js/src/user/stats/UserReports.tsx +++ b/frontend/js/src/user/stats/UserReports.tsx @@ -1,16 +1,23 @@ import * as React from "react"; import { + faBorderAll, faGlobe, faInfoCircle, faUser, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useLoaderData, useNavigate, useSearchParams } from "react-router"; +import { + Link, + useLoaderData, + useNavigate, + useSearchParams, +} from "react-router"; import { Helmet } from "react-helmet"; import Tooltip from "react-tooltip"; import NiceModal from "@ebay/nice-modal-react"; +import { Card } from "react-bootstrap"; import Pill from "../../components/Pill"; import UserListeningActivity from "./components/UserListeningActivity"; import UserTopEntity from "./components/UserTopEntity"; @@ -38,7 +45,7 @@ export default function UserReports() { const { user = undefined } = props ?? {}; // Context - const { currentUser } = React.useContext(GlobalAppContext); + const { currentUser, APIService } = React.useContext(GlobalAppContext); // Router const navigate = useNavigate(); @@ -81,6 +88,16 @@ export default function UserReports() { ); + const encodedUserOrCurrentUserName = user?.name + ? encodeURIComponent(user.name) + : encodeURIComponent(currentUser.name); + let albumStatsUrl = ""; + if (user) { + albumStatsUrl = `/user/${encodeURIComponent(user.name)}/stats`; + } else { + albumStatsUrl = `/statistics`; + } + albumStatsUrl += `/top-albums/?range=${range}`; return (
@@ -108,11 +125,7 @@ export default function UserReports() { type="button" onClick={() => { navigate( - `/user/${ - user?.name - ? encodeURIComponent(user.name) - : encodeURIComponent(currentUser.name) - }/stats/?range=${range}` + `/user/${encodedUserOrCurrentUserName}/stats/?range=${range}` ); }} className={`pill secondary ${user ? "active" : ""}`} @@ -134,7 +147,35 @@ export default function UserReports() {
{statsExplanationModalButton} - +
+
+ +
+
+ + + + Visualize & share{" "} + + + + +
{statsExplanationModalButton} @@ -153,6 +194,20 @@ export default function UserReports() { entity="release-group" user={user} terminology="album" + extraButtons={[ + + Visualize & share{" "} + + , + ]} />
diff --git a/frontend/js/src/user/stats/components/UserArtistActivity.tsx b/frontend/js/src/user/stats/components/UserArtistActivity.tsx index 51a80822de..94d97208ed 100644 --- a/frontend/js/src/user/stats/components/UserArtistActivity.tsx +++ b/frontend/js/src/user/stats/components/UserArtistActivity.tsx @@ -5,6 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { useQuery } from "@tanstack/react-query"; import { useNavigate, Link } from "react-router"; +import { useMediaQuery } from "react-responsive"; import Card from "../../../components/Card"; import Loader from "../../../components/Loader"; import GlobalAppContext from "../../../utils/GlobalAppContext"; @@ -50,6 +51,7 @@ function CustomTooltip({ export default function UserArtistActivity(props: UserArtistActivityProps) { const { APIService } = React.useContext(GlobalAppContext); const navigate = useNavigate(); + const isMobile = useMediaQuery({ maxWidth: 767 }); // Props const { user, range } = props; @@ -190,7 +192,12 @@ export default function UserArtistActivity(props: UserArtistActivityProps) { ) )} indexBy="label" - margin={{ top: 20, right: 80, bottom: 100, left: 120 }} + margin={{ + top: 20, + right: isMobile ? 0 : 80, + bottom: 100, + left: isMobile ? 20 : 120, + }} padding={0.2} layout="vertical" colors={{ scheme: "nivo" }} diff --git a/frontend/js/src/user/stats/components/UserListeningActivity.tsx b/frontend/js/src/user/stats/components/UserListeningActivity.tsx index 6133c979ec..eb38762be2 100644 --- a/frontend/js/src/user/stats/components/UserListeningActivity.tsx +++ b/frontend/js/src/user/stats/components/UserListeningActivity.tsx @@ -432,11 +432,7 @@ export default function UserListeningActivity( const formattedAvgListens = new Intl.NumberFormat().format(avgListens); return ( - +

Listening Activity

+

Top {entityTextOnCard}

{" "} @@ -85,10 +96,14 @@ export default function UserTopEntity(props: UserTopEntityProps) { } return ( - +

Top {entityTextOnCard}

-
+
{entity === "artist" && Object.keys(data).length > 0 && (data as UserArtistsResponse).payload.artists.map( @@ -274,10 +289,11 @@ export default function UserTopEntity(props: UserTopEntityProps) { } )}
-
+
View more… + {extraButtons}
diff --git a/listenbrainz/webserver/templates/art/svg-templates/designer-top-10-alt.svg b/listenbrainz/webserver/templates/art/svg-templates/designer-top-10-alt.svg index 6b5af35f4e..f67511ec07 100644 --- a/listenbrainz/webserver/templates/art/svg-templates/designer-top-10-alt.svg +++ b/listenbrainz/webserver/templates/art/svg-templates/designer-top-10-alt.svg @@ -10,7 +10,9 @@ viewBox="0 0 924 924" xmlns:dc="http://purl.org/dc/elements/1.1/" height="{{ height }}" - width="{{ width }}"> + width="{{ width }}" + preserveAspectRatio="xMinYMin meet" +> Top 10 releases {{ metadata["time_range"] }} for {{ metadata["user_name"] }} {%- for release in releases[:10] -%} diff --git a/listenbrainz/webserver/templates/art/svg-templates/designer-top-10.svg b/listenbrainz/webserver/templates/art/svg-templates/designer-top-10.svg index 8786cfb038..d3449f85bd 100644 --- a/listenbrainz/webserver/templates/art/svg-templates/designer-top-10.svg +++ b/listenbrainz/webserver/templates/art/svg-templates/designer-top-10.svg @@ -10,7 +10,9 @@ viewBox="0 0 924 924" xmlns:dc="http://purl.org/dc/elements/1.1/" height="{{ height }}" - width="{{ width }}"> + width="{{ width }}" + preserveAspectRatio="xMinYMin meet" +> Top 10 releases {{ metadata["time_range"] }} for {{ metadata["user_name"] }} {%- for release in releases[:10] -%} diff --git a/listenbrainz/webserver/templates/art/svg-templates/designer-top-5.svg b/listenbrainz/webserver/templates/art/svg-templates/designer-top-5.svg index 1e29e9eba2..80487e6bd4 100644 --- a/listenbrainz/webserver/templates/art/svg-templates/designer-top-5.svg +++ b/listenbrainz/webserver/templates/art/svg-templates/designer-top-5.svg @@ -10,7 +10,9 @@ viewBox="0 0 924 924" xmlns:dc="http://purl.org/dc/elements/1.1/" height="{{ height }}" - width="{{ width }}"> + width="{{ width }}" + preserveAspectRatio="xMinYMin meet" +> Top 5 artists {{ metadata["time_range"] }} for {{ metadata["user_name"] }} {%- for artist in artists[:5] -%} diff --git a/listenbrainz/webserver/templates/art/svg-templates/lps-on-the-floor.svg b/listenbrainz/webserver/templates/art/svg-templates/lps-on-the-floor.svg index b37afcda9e..4a6fcf331e 100644 --- a/listenbrainz/webserver/templates/art/svg-templates/lps-on-the-floor.svg +++ b/listenbrainz/webserver/templates/art/svg-templates/lps-on-the-floor.svg @@ -25,7 +25,9 @@ xmlns="http://www.w3.org/2000/svg" role="img" width="{{ width }}" height="{{ height }}" - viewBox="0 0 {{ width }} {{ height }}"> + viewBox="0 0 {{ width }} {{ height }}" + preserveAspectRatio="xMinYMin meet" +> Top 5 releases {{ metadata["time_range"] }} for {{ metadata["user_name"] }} {%- for release in releases[:5] -%} diff --git a/listenbrainz/webserver/templates/art/svg-templates/simple-grid.svg b/listenbrainz/webserver/templates/art/svg-templates/simple-grid.svg index 18f953d581..dc290ca2e4 100644 --- a/listenbrainz/webserver/templates/art/svg-templates/simple-grid.svg +++ b/listenbrainz/webserver/templates/art/svg-templates/simple-grid.svg @@ -6,7 +6,9 @@ role="img" viewBox="0 0 {{ width }} {{ height }}" width="{{ width }}" - height="{{ height }}"> + height="{{ height }}" + preserveAspectRatio="xMinYMin meet" +> {% if title is defined %} {{ title }}