Skip to content

Commit

Permalink
feat: Cache User Stats
Browse files Browse the repository at this point in the history
  • Loading branch information
anshg1214 committed May 26, 2024
1 parent a116949 commit 36e5b01
Show file tree
Hide file tree
Showing 8 changed files with 1,559 additions and 1,658 deletions.
1 change: 1 addition & 0 deletions frontend/js/src/user/stats/UserReports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export default class UserReports extends React.Component<
{Array.from(ranges, ([stat_type, stat_name]) => {
return (
<Pill
key={`${stat_type}-${stat_name}`}
active={range === stat_type}
type="secondary"
onClick={() => this.changeRange(stat_type)}
Expand Down
303 changes: 130 additions & 173 deletions frontend/js/src/user/stats/components/UserArtistMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import { faExclamationCircle, faLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";

import APIService from "../../../utils/APIService";
import { useQuery } from "@tanstack/react-query";
import _ from "lodash";
import Card from "../../../components/Card";
import Loader from "../../../components/Loader";
import Choropleth from "./Choropleth";
import { isInvalidStatRange } from "../utils";
import { COLOR_BLACK } from "../../../utils/constants";
import GlobalAppContext from "../../../utils/GlobalAppContext";

export type UserArtistMapProps = {
range: UserStatsAPIRange;
user?: ListenBrainzUser;
// eslint-disable-next-line react/no-unused-prop-types
apiUrl: string;
};

Expand All @@ -25,96 +28,50 @@ export type UserArtistMapState = {
selectedMetric: "artist" | "listen";
};

export default class UserArtistMap extends React.Component<
UserArtistMapProps,
UserArtistMapState
> {
APIService: APIService;

rawData: UserArtistMapResponse;

constructor(props: UserArtistMapProps) {
super(props);
this.APIService = new APIService(
props.apiUrl || `${window.location.origin}/1`
);

this.state = {
data: [],
loading: false,
errorMessage: "",
hasError: false,
selectedMetric: "artist",
};

this.rawData = {} as UserArtistMapResponse;
}

componentDidUpdate(prevProps: UserArtistMapProps) {
const { range: prevRange, user: prevUser } = prevProps;
const { range: currRange, user: currUser } = this.props;
if (prevRange !== currRange || prevUser !== currUser) {
if (isInvalidStatRange(currRange)) {
this.setState({
loading: false,
export default function UserArtistMap(props: UserArtistMapProps) {
const { APIService } = React.useContext(GlobalAppContext);

// Props
const { user, range } = props;

const { data: loaderData, isLoading: loading } = useQuery({
queryKey: ["user-artist-map", range, user?.name],
queryFn: async () => {
try {
if (!range || isInvalidStatRange(range)) {
return {
data: {},
hasError: true,
errorMessage: `Invalid range: ${range}`,
};
}
const queryData = await APIService.getUserArtistMap(user?.name, range);
return {
data: queryData,
hasError: false,
errorMessage: "",
};
} catch (error) {
return {
data: {},
hasError: true,
errorMessage: `Invalid range: ${currRange}`,
});
} else {
this.loadData();
errorMessage: error.message,
};
}
}
}

changeSelectedMetric = (
newSelectedMetric: "artist" | "listen",
event?: React.MouseEvent<HTMLElement>
) => {
if (event) {
event.preventDefault();
}

this.setState({
selectedMetric: newSelectedMetric,
data: this.processData(this.rawData, newSelectedMetric),
});
};
},
});

loadData = async (): Promise<void> => {
const { selectedMetric } = this.state;
this.setState({
hasError: false,
loading: true,
});
this.rawData = await this.getData();
this.setState({
data: this.processData(this.rawData, selectedMetric),
loading: false,
});
};
const { data: rawData = {}, hasError = false, errorMessage = "" } =
loaderData || {};

getData = async (): Promise<UserArtistMapResponse> => {
const { range, user } = this.props;
try {
return await this.APIService.getUserArtistMap(user?.name, range);
} catch (error) {
this.setState({
loading: false,
hasError: true,
errorMessage: error.message,
});
}
return {} as UserArtistMapResponse;
};

processData = (
data: UserArtistMapResponse,
selectedMetric: "artist" | "listen"
const processData = (
selectedMetric: "artist" | "listen",
unprocessedData?: UserArtistMapResponse
): UserArtistMapData => {
if (!data?.payload) {
if (!unprocessedData?.payload) {
return [];
}
return data.payload.artist_map.map((country) => {
return unprocessedData.payload.artist_map.map((country) => {
return {
id: country.country,
value:
Expand All @@ -126,99 +83,99 @@ export default class UserArtistMap extends React.Component<
});
};

render() {
const {
selectedMetric,
data,
errorMessage,
hasError,
loading,
} = this.state;
const [selectedMetric, setSelectedMetric] = React.useState<
"artist" | "listen"
>("artist");

return (
<Card className="user-stats-card">
<div className="row">
<div className="col-md-9 col-xs-6">
<h3 style={{ marginLeft: 20 }}>
<span className="capitalize-bold">Artist Origins</span>
<small>&nbsp;Click on a country to see more details</small>
</h3>
</div>
<div
className="col-md-2 col-xs-4 text-right"
style={{ marginTop: 20 }}
>
<span>Rank by</span>
<span className="dropdown">
<button
className="dropdown-toggle btn-transparent capitalize-bold"
data-toggle="dropdown"
type="button"
const [data, setData] = React.useState<UserArtistMapData>([]);

React.useEffect(() => {
if (rawData && rawData?.payload) {
const newProcessedData = processData(selectedMetric, rawData);
setData(newProcessedData);
}
}, [rawData, selectedMetric]);

const changeSelectedMetric = (newSelectedMetric: "artist" | "listen") => {
setSelectedMetric(newSelectedMetric);
};

return (
<Card className="user-stats-card">
<div className="row">
<div className="col-md-9 col-xs-6">
<h3 style={{ marginLeft: 20 }}>
<span className="capitalize-bold">Artist Origins</span>
<small>&nbsp;Click on a country to see more details</small>
</h3>
</div>
<div className="col-md-2 col-xs-4 text-right" style={{ marginTop: 20 }}>
<span>Rank by</span>
<span className="dropdown">
<button
className="dropdown-toggle btn-transparent capitalize-bold"
data-toggle="dropdown"
type="button"
>
{selectedMetric}s
<span className="caret" />
</button>
<ul className="dropdown-menu" role="menu">
<li
className={selectedMetric === "listen" ? "active" : undefined}
>
{selectedMetric}s
<span className="caret" />
</button>
<ul className="dropdown-menu" role="menu">
<li
className={selectedMetric === "listen" ? "active" : undefined}
<a
href=""
role="button"
onClick={() => changeSelectedMetric("listen")}
>
<a
href=""
role="button"
onClick={(event) =>
this.changeSelectedMetric("listen", event)
}
>
Listens
</a>
</li>
<li
className={selectedMetric === "artist" ? "active" : undefined}
Listens
</a>
</li>
<li
className={selectedMetric === "artist" ? "active" : undefined}
>
<a
href=""
role="button"
onClick={() => changeSelectedMetric("artist")}
>
<a
href=""
role="button"
onClick={(event) =>
this.changeSelectedMetric("artist", event)
}
>
Artists
</a>
</li>
</ul>
Artists
</a>
</li>
</ul>
</span>
</div>
<div className="col-md-1 col-xs-2 text-right">
<h4 style={{ marginTop: 20 }}>
<a href="#artist-origin">
<FontAwesomeIcon
icon={faLink as IconProp}
size="sm"
color={COLOR_BLACK}
style={{ marginRight: 20 }}
/>
</a>
</h4>
</div>
</div>
<Loader isLoading={loading}>
{hasError ? (
<div
className="flex-center"
style={{
minHeight: "inherit",
}}
>
<span style={{ fontSize: 24 }} className="text-center">
<FontAwesomeIcon icon={faExclamationCircle as IconProp} />{" "}
{errorMessage}
</span>
</div>
<div className="col-md-1 col-xs-2 text-right">
<h4 style={{ marginTop: 20 }}>
<a href="#artist-origin">
<FontAwesomeIcon
icon={faLink as IconProp}
size="sm"
color={COLOR_BLACK}
style={{ marginRight: 20 }}
/>
</a>
</h4>
</div>
</div>
<Loader isLoading={loading}>
{hasError ? (
<div
className="flex-center"
style={{
minHeight: "inherit",
}}
>
<span style={{ fontSize: 24 }} className="text-center">
<FontAwesomeIcon icon={faExclamationCircle as IconProp} />{" "}
{errorMessage}
</span>
</div>
) : (
<Choropleth data={data} selectedMetric={selectedMetric} />
)}
</Loader>
</Card>
);
}
) : (
<Choropleth data={data} selectedMetric={selectedMetric} />
)}
</Loader>
</Card>
);
}
Loading

0 comments on commit 36e5b01

Please sign in to comment.