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 }}