mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 21:21:02 +00:00
feat: add top-artists report
This commit is contained in:
parent
6a6ba493f6
commit
6fc10c40ca
18 changed files with 345 additions and 30 deletions
|
|
@ -25,6 +25,9 @@ export const NavBar: React.FC = () => {
|
|||
<Link to="/reports/listens">
|
||||
<NavItem>Listens Report</NavItem>
|
||||
</Link>
|
||||
<Link to="/reports/top-artists">
|
||||
<NavItem>Top Artists</NavItem>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
65
frontend/src/components/ReportOptions.tsx
Normal file
65
frontend/src/components/ReportOptions.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import React from "react";
|
||||
import { TimePreset } from "../api/entities/time-preset.enum";
|
||||
import { TopArtistsOptions } from "../api/entities/top-artists-options";
|
||||
import { DateSelect } from "./inputs/DateSelect";
|
||||
|
||||
interface ReportOptionsProps {
|
||||
reportOptions: TopArtistsOptions;
|
||||
setReportOptions: (options: TopArtistsOptions) => void;
|
||||
}
|
||||
|
||||
const timePresetOptions = [
|
||||
{ value: TimePreset.LAST_7_DAYS, description: "Last 7 days" },
|
||||
{ value: TimePreset.LAST_30_DAYS, description: "Last 30 days" },
|
||||
{ value: TimePreset.LAST_90_DAYS, description: "Last 90 days" },
|
||||
{ value: TimePreset.LAST_180_DAYS, description: "Last 180 days" },
|
||||
{ value: TimePreset.LAST_365_DAYS, description: "Last 365 days" },
|
||||
{ value: TimePreset.ALL_TIME, description: "All time" },
|
||||
{ value: TimePreset.CUSTOM, description: "Custom" },
|
||||
];
|
||||
|
||||
export const ReportOptions: React.FC<ReportOptionsProps> = ({
|
||||
reportOptions,
|
||||
setReportOptions,
|
||||
}) => {
|
||||
return (
|
||||
<div className="md:flex">
|
||||
<div className="text-gray-700">
|
||||
<label className="text-sm">Timeframe</label>
|
||||
<select
|
||||
className="block appearance-none min-w-full md:w-1/4 bg-white border border-gray-400 hover:border-gray-500 p-2 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
|
||||
onChange={(e) =>
|
||||
setReportOptions({
|
||||
...reportOptions,
|
||||
timePreset: e.target.value as TimePreset,
|
||||
})
|
||||
}
|
||||
>
|
||||
{timePresetOptions.map(({ value, description }) => (
|
||||
<option value={value} key={value}>
|
||||
{description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{reportOptions.timePreset === TimePreset.CUSTOM && (
|
||||
<div className="md:flex text-gray-700">
|
||||
<DateSelect
|
||||
label="Start"
|
||||
value={reportOptions.customTimeStart}
|
||||
onChange={(newDate) =>
|
||||
setReportOptions({ ...reportOptions, customTimeStart: newDate })
|
||||
}
|
||||
/>
|
||||
<DateSelect
|
||||
label="End"
|
||||
value={reportOptions.customTimeEnd}
|
||||
onChange={(newDate) =>
|
||||
setReportOptions({ ...reportOptions, customTimeEnd: newDate })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
62
frontend/src/components/ReportTopArtists.tsx
Normal file
62
frontend/src/components/ReportTopArtists.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import React, { useMemo, useState } from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { getTopArtists } from "../api/api";
|
||||
import { TimePreset } from "../api/entities/time-preset.enum";
|
||||
import { TopArtistsOptions } from "../api/entities/top-artists-options";
|
||||
import { useAsync } from "../hooks/use-async";
|
||||
import { useAuth } from "../hooks/use-auth";
|
||||
import { ReportOptions } from "./ReportOptions";
|
||||
|
||||
export const ReportTopArtists: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
const [reportOptions, setReportOptions] = useState<TopArtistsOptions>({
|
||||
timePreset: TimePreset.LAST_90_DAYS,
|
||||
customTimeStart: new Date(0),
|
||||
customTimeEnd: new Date(),
|
||||
});
|
||||
|
||||
const fetchData = useMemo(() => () => getTopArtists(reportOptions), [
|
||||
reportOptions,
|
||||
]);
|
||||
|
||||
const { value: report, pending: isLoading } = useAsync(fetchData, []);
|
||||
|
||||
const reportHasItems = !isLoading && report.length !== 0;
|
||||
|
||||
if (!user) {
|
||||
return <Redirect to="/" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="md:flex md:justify-center p-4">
|
||||
<div className="md:flex-shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-2xl font-normal text-gray-700">Top Artists</p>
|
||||
</div>
|
||||
<div className="shadow-xl bg-gray-100 rounded p-5 m-2">
|
||||
<ReportOptions
|
||||
reportOptions={reportOptions}
|
||||
setReportOptions={setReportOptions}
|
||||
/>
|
||||
{isLoading && (
|
||||
<div>
|
||||
<div className="loader rounded-full border-8 h-64 w-64"></div>
|
||||
</div>
|
||||
)}
|
||||
{!reportHasItems && (
|
||||
<div>
|
||||
<p>Report is emtpy! :(</p>
|
||||
</div>
|
||||
)}
|
||||
{reportHasItems &&
|
||||
report.map(({ artist, count }) => (
|
||||
<div>
|
||||
{count} - {artist.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
31
frontend/src/components/inputs/DateSelect.tsx
Normal file
31
frontend/src/components/inputs/DateSelect.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { format, parse } from "date-fns";
|
||||
import React from "react";
|
||||
|
||||
const parseDateFromDateInput = (input: string) =>
|
||||
parse(input, "yyyy-MM-dd", new Date());
|
||||
|
||||
const formatDateForDateInput = (date: Date) => format(date, "yyyy-MM-dd");
|
||||
|
||||
interface DateSelectProps {
|
||||
label: string;
|
||||
value: Date;
|
||||
onChange: (date: Date) => void;
|
||||
}
|
||||
|
||||
export const DateSelect: React.FC<DateSelectProps> = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-sm">{label}</label>
|
||||
<input
|
||||
className="block appearance-none min-w-full md:win-w-0 md:w-1/4 bg-white border border-gray-400 hover:border-gray-500 p-2 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
|
||||
type="date"
|
||||
value={formatDateForDateInput(value)}
|
||||
onChange={(e) => onChange(parseDateFromDateInput(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue