Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Misc SPA fixes, part 2 #2866

Merged
merged 14 commits into from
May 10, 2024
3 changes: 2 additions & 1 deletion frontend/js/src/error/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export function ErrorBoundary() {
<Helmet>
<title>Error</title>
</Helmet>
<h2 className="page-title">Error Occured!</h2>
<h2 className="page-title">An error occured</h2>
<p>{errorMessage}</p>
</>
);
}
Expand Down
197 changes: 101 additions & 96 deletions frontend/js/src/explore/fresh-releases/components/ReleaseTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,95 @@ type ReleaseTimelineProps = {
direction: SortDirection;
};

function createMarks(
releases: Array<FreshReleaseItem>,
sortDirection: string,
order: string
) {
let dataArr: Array<string> = [];
let percentArr: Array<number> = [];
// We want to filter out the keys that have less than 1.5% of the total releases
const minReleasesThreshold = Math.floor(releases.length * 0.015);
if (order === "release_date") {
const releasesPerDate = countBy(
releases,
(item: FreshReleaseItem) => item.release_date
);
const filteredDates = Object.keys(releasesPerDate).filter(
(date) => releasesPerDate[date] >= minReleasesThreshold
);

dataArr = filteredDates.map((item) => formatReleaseDate(item));
percentArr = filteredDates
.map((item) => (releasesPerDate[item] / releases.length) * 100)
.map((_, index, arr) =>
arr.slice(0, index + 1).reduce((prev, curr) => prev + curr)
);
} else if (order === "artist_credit_name") {
const artistInitialsCount = countBy(releases, (item: FreshReleaseItem) =>
item.artist_credit_name.charAt(0).toUpperCase()
);
const filteredInitials = Object.keys(artistInitialsCount).filter(
(initial) => artistInitialsCount[initial] >= minReleasesThreshold
);

dataArr = filteredInitials.sort();
percentArr = filteredInitials
.map((item) => (artistInitialsCount[item] / releases.length) * 100)
.map((_, index, arr) =>
arr.slice(0, index + 1).reduce((prev, curr) => prev + curr)
);
} else if (order === "release_name") {
const releaseInitialsCount = countBy(releases, (item: FreshReleaseItem) =>
item.release_name.charAt(0).toUpperCase()
);
const filteredInitials = Object.keys(releaseInitialsCount).filter(
(initial) => releaseInitialsCount[initial] >= minReleasesThreshold
);

dataArr = filteredInitials.sort();
percentArr = filteredInitials
.map((item) => (releaseInitialsCount[item] / releases.length) * 100)
.map((_, index, arr) =>
arr.slice(0, index + 1).reduce((prev, curr) => prev + curr)
);
} else {
// conutBy gives us an asc-sorted Dict by confidence
const confidenceInitialsCount = countBy(
releases,
(item: FreshReleaseItem) => item?.confidence
);
dataArr = Object.keys(confidenceInitialsCount);
percentArr = Object.values(confidenceInitialsCount)
.map((item) => (item / releases.length) * 100)
.map((_, index, arr) =>
arr.slice(0, index + 1).reduce((prev, curr) => prev + curr)
);
}

if (sortDirection === "descend") {
dataArr.reverse();
percentArr = percentArr.reverse().map((v) => (v <= 100 ? 100 - v : 0));
}

/**
* We want the timeline dates or marks to start where the grid starts.
* So the 0% should always have the first date. Therefore we use unshift(0) here.
* With the same logic, we don't want the last date to be at 100% because
* that will mean we're at the bottom of the grid.
* The last date should start before 100%. That explains the pop().
* For descending sort, the reverse computation above possibly already ensures that
* the percentArr starts with 0 and ends with a non-100% value, which is desired.
* Hence, we add a check to skip the unshift(0) and pop() operations in that case.
*/
if (percentArr[0] !== 0) {
percentArr.unshift(0);
percentArr.pop();
}

return zipObject(percentArr, dataArr);
}

export default function ReleaseTimeline(props: ReleaseTimelineProps) {
const { releases, order, direction } = props;

Expand All @@ -29,108 +118,24 @@ export default function ReleaseTimeline(props: ReleaseTimelineProps) {
return scrollTo;
}, []);

function createMarks(data: Array<FreshReleaseItem>, sortDirection: string) {
let dataArr: Array<string> = [];
let percentArr: Array<number> = [];
// We want to filter out the keys that have less than 1.5% of the total releases
const minReleasesThreshold = Math.floor(data.length * 0.015);
if (order === "release_date") {
const releasesPerDate = countBy(
releases,
(item: FreshReleaseItem) => item.release_date
);
const filteredDates = Object.keys(releasesPerDate).filter(
(date) => releasesPerDate[date] >= minReleasesThreshold
);

dataArr = filteredDates.map((item) => formatReleaseDate(item));
percentArr = filteredDates
.map((item) => (releasesPerDate[item] / data.length) * 100)
.map((_, index, arr) =>
arr.slice(0, index + 1).reduce((prev, curr) => prev + curr)
);
} else if (order === "artist_credit_name") {
const artistInitialsCount = countBy(data, (item: FreshReleaseItem) =>
item.artist_credit_name.charAt(0).toUpperCase()
);
const filteredInitials = Object.keys(artistInitialsCount).filter(
(initial) => artistInitialsCount[initial] >= minReleasesThreshold
);

dataArr = filteredInitials.sort();
percentArr = filteredInitials
.map((item) => (artistInitialsCount[item] / data.length) * 100)
.map((_, index, arr) =>
arr.slice(0, index + 1).reduce((prev, curr) => prev + curr)
);
} else if (order === "release_name") {
const releaseInitialsCount = countBy(data, (item: FreshReleaseItem) =>
item.release_name.charAt(0).toUpperCase()
);
const filteredInitials = Object.keys(releaseInitialsCount).filter(
(initial) => releaseInitialsCount[initial] >= minReleasesThreshold
);

dataArr = filteredInitials.sort();
percentArr = filteredInitials
.map((item) => (releaseInitialsCount[item] / data.length) * 100)
.map((_, index, arr) =>
arr.slice(0, index + 1).reduce((prev, curr) => prev + curr)
);
} else {
// conutBy gives us an asc-sorted Dict by confidence
const confidenceInitialsCount = countBy(
data,
(item: FreshReleaseItem) => item?.confidence
);
dataArr = Object.keys(confidenceInitialsCount);
percentArr = Object.values(confidenceInitialsCount)
.map((item) => (item / data.length) * 100)
.map((_, index, arr) =>
arr.slice(0, index + 1).reduce((prev, curr) => prev + curr)
);
}

if (sortDirection === "descend") {
dataArr.reverse();
percentArr = percentArr.reverse().map((v) => (v <= 100 ? 100 - v : 0));
}

/**
* We want the timeline dates or marks to start where the grid starts.
* So the 0% should always have the first date. Therefore we use unshift(0) here.
* With the same logic, we don't want the last date to be at 100% because
* that will mean we're at the bottom of the grid.
* The last date should start before 100%. That explains the pop().
* For descending sort, the reverse computation above possibly already ensures that
* the percentArr starts with 0 and ends with a non-100% value, which is desired.
* Hence, we add a check to skip the unshift(0) and pop() operations in that case.
*/
if (percentArr[0] !== 0) {
percentArr.unshift(0);
percentArr.pop();
}

return zipObject(percentArr, dataArr);
}
React.useEffect(() => {
setMarks(createMarks(releases, direction, order));
}, [releases, direction, order]);

const handleScroll = React.useCallback(
debounce(() => {
// TODO change to relative position of #release-cards-grid instead of window
React.useEffect(() => {
const handleScroll = debounce(() => {
const container = document.getElementById("release-card-grids");
if (!container) {
return;
}
const scrollPos =
(window.scrollY / document.documentElement.scrollHeight) * 100;
((window.scrollY - container.offsetTop) / container.scrollHeight) * 100;
setCurrentValue(scrollPos);
}, 300),
[]
);
}, 500);

React.useEffect(() => {
setMarks(createMarks(releases, direction));
}, [releases, direction]);

React.useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
handleScroll.cancel();
window.removeEventListener("scroll", handleScroll);
};
}, []);
Expand Down
61 changes: 24 additions & 37 deletions frontend/js/src/user/stats/UserReports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { useLoaderData, useNavigate } from "react-router-dom";
import type { NavigateFunction } from "react-router-dom";
import { Helmet } from "react-helmet";

import ErrorBoundary from "../../utils/ErrorBoundary";
import Pill from "../../components/Pill";
import UserListeningActivity from "./components/UserListeningActivity";
import UserTopEntity from "./components/UserTopEntity";
Expand Down Expand Up @@ -170,58 +169,46 @@ export default class UserReports extends React.Component<
</a>
</small>
<section id="listening-activity">
<ErrorBoundary>
<UserListeningActivity range={range} apiUrl={apiUrl} user={user} />
</ErrorBoundary>
<UserListeningActivity range={range} apiUrl={apiUrl} user={user} />
</section>
<section id="top-entity">
<div className="row">
<div className="col-md-4">
<ErrorBoundary>
<UserTopEntity
range={range}
entity="artist"
apiUrl={apiUrl}
user={user}
terminology="artist"
/>
</ErrorBoundary>
<UserTopEntity
range={range}
entity="artist"
apiUrl={apiUrl}
user={user}
terminology="artist"
/>
</div>
<div className="col-md-4">
<ErrorBoundary>
<UserTopEntity
range={range}
entity="release-group"
apiUrl={apiUrl}
user={user}
terminology="album"
/>
</ErrorBoundary>
<UserTopEntity
range={range}
entity="release-group"
apiUrl={apiUrl}
user={user}
terminology="album"
/>
</div>
<div className="col-md-4">
<ErrorBoundary>
<UserTopEntity
range={range}
entity="recording"
apiUrl={apiUrl}
user={user}
terminology="track"
/>
</ErrorBoundary>
<UserTopEntity
range={range}
entity="recording"
apiUrl={apiUrl}
user={user}
terminology="track"
/>
</div>
</div>
</section>
{user && (
<section id="daily-activity">
<ErrorBoundary>
<UserDailyActivity range={range} apiUrl={apiUrl} user={user} />
</ErrorBoundary>
<UserDailyActivity range={range} apiUrl={apiUrl} user={user} />
</section>
)}
<section id="artist-origin">
<ErrorBoundary>
<UserArtistMap range={range} apiUrl={apiUrl} user={user} />
</ErrorBoundary>
<UserArtistMap range={range} apiUrl={apiUrl} user={user} />
</section>
</div>
);
Expand Down
15 changes: 5 additions & 10 deletions frontend/js/src/user/stats/components/UserArtistMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,11 @@ export default class UserArtistMap extends React.Component<
try {
return await this.APIService.getUserArtistMap(user?.name, range);
} catch (error) {
if (error.response && error.response.status === 204) {
this.setState({
loading: false,
hasError: true,
errorMessage:
"There are no statistics available for this user for this period",
});
} else {
throw error;
}
this.setState({
loading: false,
hasError: true,
errorMessage: error.message,
});
}
return {} as UserArtistMapResponse;
};
Expand Down
15 changes: 5 additions & 10 deletions frontend/js/src/user/stats/components/UserDailyActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,11 @@ export default class UserDailyActivity extends React.Component<
const data = await this.APIService.getUserDailyActivity(user.name, range);
return data;
} catch (error) {
if (error.response && error.response.status === 204) {
this.setState({
loading: false,
hasError: true,
errorMessage:
"There are no statistics available for this user for this period",
});
} else {
throw error;
}
this.setState({
loading: false,
hasError: true,
errorMessage: error.message,
});
}
return {} as UserDailyActivityResponse;
};
Expand Down
15 changes: 5 additions & 10 deletions frontend/js/src/user/stats/components/UserListeningActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,11 @@ export default class UserListeningActivity extends React.Component<
try {
return await this.APIService.getUserListeningActivity(user?.name, range);
} catch (error) {
if (error.response && error.response.status === 204) {
this.setState({
loading: false,
hasError: true,
errorMessage:
"There are no statistics available for this user for this period",
});
} else {
throw error;
}
this.setState({
loading: false,
hasError: true,
errorMessage: error.message,
});
}
return {} as UserListeningActivityResponse;
};
Expand Down