Add Back Button to StatView page
Return to the `Home` page by pressing a back button (lucide-react icon) on the `StatView` page This commit also includes some general formatting
This commit is contained in:
10
app/package-lock.json
generated
10
app/package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lucide-react": "^0.544.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-range-slider-input": "^3.2.1",
|
"react-range-slider-input": "^3.2.1",
|
||||||
@@ -3177,6 +3178,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.544.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz",
|
||||||
|
"integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lucide-react": "^0.544.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-range-slider-input": "^3.2.1",
|
"react-range-slider-input": "^3.2.1",
|
||||||
|
|||||||
@@ -1,139 +1,143 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { getAllStreams } from '../util/db';
|
import { getAllStreams } from "../util/db";
|
||||||
import { formatSeconds, tsToDate, getDaysBetween, getDaysAfter, dateToTs } from '../util/time';
|
import { formatSeconds, tsToDate, getDaysBetween, getDaysAfter, dateToTs } from "../util/time";
|
||||||
import RangeSlider from 'react-range-slider-input';
|
import RangeSlider from "react-range-slider-input";
|
||||||
import 'react-range-slider-input/dist/style.css';
|
import "react-range-slider-input/dist/style.css";
|
||||||
import type { stream } from '../model/types';
|
import type { stream } from "../model/types";
|
||||||
import { getFirstStreamDate, getLastStreamDate, getListenedTracks, getListenedArtists } from '../model/parser';
|
import { getFirstStreamDate, getLastStreamDate, getListenedTracks, getListenedArtists } from "../model/parser";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ArrowLeft as Back } from "lucide-react";
|
||||||
|
|
||||||
const StatView = () => {
|
const StatView = () => {
|
||||||
const [streams, setStreams] = useState<stream[]>([]);
|
const [streams, setStreams] = useState<stream[]>([]);
|
||||||
const [mostListenedSongs, setMostListenedSongs] = useState<stream[]>([]);
|
const [mostListenedSongs, setMostListenedSongs] = useState<stream[]>([]);
|
||||||
const [mostListenedArtists, setMostListenedArtists] = useState<stream[]>([]);
|
const [mostListenedArtists, setMostListenedArtists] = useState<stream[]>([]);
|
||||||
|
|
||||||
const [firstStreamDate, setFirstStreamDate] = useState<Date>();
|
const [firstStreamDate, setFirstStreamDate] = useState<Date>();
|
||||||
const [lastStreamDate, setLastStreamDate] = useState<Date>();
|
const [lastStreamDate, setLastStreamDate] = useState<Date>();
|
||||||
|
|
||||||
const [dateRange, setDateRange] = useState<[Date, Date]>([new Date(), new Date()]);
|
const [dateRange, setDateRange] = useState<[Date, Date]>([new Date(), new Date()]);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAllStreams().then((streams) => {
|
getAllStreams()
|
||||||
setStreams(streams);
|
.then((streams) => {
|
||||||
if (streams.length > 0) {
|
setStreams(streams);
|
||||||
const firstDate = tsToDate(getFirstStreamDate(streams));
|
if (streams.length > 0) {
|
||||||
const lastDate = tsToDate(getLastStreamDate(streams));
|
const firstDate = tsToDate(getFirstStreamDate(streams));
|
||||||
setFirstStreamDate(firstDate);
|
const lastDate = tsToDate(getLastStreamDate(streams));
|
||||||
setLastStreamDate(lastDate);
|
setFirstStreamDate(firstDate);
|
||||||
setDateRange([firstDate, lastDate]);
|
setLastStreamDate(lastDate);
|
||||||
}
|
setDateRange([firstDate, lastDate]);
|
||||||
}).catch((error) => {
|
}
|
||||||
console.error('Error fetching streams:', error);
|
})
|
||||||
});
|
.catch((error) => {
|
||||||
}, []);
|
console.error("Error fetching streams:", error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMostListenedSongs(getListenedTracks(streams, dateToTs(dateRange[0]), dateToTs(dateRange[1]), 100));
|
setMostListenedSongs(getListenedTracks(streams, dateToTs(dateRange[0]), dateToTs(dateRange[1]), 100));
|
||||||
setMostListenedArtists(getListenedArtists(streams, dateToTs(dateRange[0]), dateToTs(dateRange[1]), 100));
|
setMostListenedArtists(getListenedArtists(streams, dateToTs(dateRange[0]), dateToTs(dateRange[1]), 100));
|
||||||
}, [dateRange]);
|
}, [dateRange]);
|
||||||
|
|
||||||
if (streams.length === 0 || !firstStreamDate || !lastStreamDate) {
|
if (streams.length === 0 || !firstStreamDate || !lastStreamDate) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row justify-between items-start gap-4 p-4">
|
<div className="flex flex-row justify-between items-start gap-4 p-4">
|
||||||
|
<Back className="absolute top-4 left-4 cursor-pointer hover:opacity-70" size={40} onClick={() => navigate(-1)} />
|
||||||
|
|
||||||
<div className="w-150 h-180 overflow-auto">
|
<div className="w-150 h-180 overflow-auto">
|
||||||
<h1>Track Statistics</h1>
|
<h1>Track Statistics</h1>
|
||||||
<ul className="truncate">
|
<ul className="truncate">
|
||||||
{mostListenedSongs.map((track, index) => (
|
{mostListenedSongs.map((track, index) => (
|
||||||
<li key={index} className="w-full h-6 flex items-center justify-between overflow-hidden hover:underline">
|
<li key={index} className="w-full h-6 flex items-center justify-between overflow-hidden hover:underline">
|
||||||
<div className="text-left truncate">
|
<div className="text-left truncate">
|
||||||
{index + 1}. {track.master_metadata_track_name}
|
{index + 1}. {track.master_metadata_track_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right flex-shrink-0 ml-2">
|
<div className="text-right flex-shrink-0 ml-2">
|
||||||
{formatSeconds(track.ms_played / 1000)}
|
{formatSeconds(track.ms_played / 1000)}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-150 h-180 overflow-auto">
|
<div className="w-150 h-180 overflow-auto">
|
||||||
<h1>Artist Statistics</h1>
|
<h1>Artist Statistics</h1>
|
||||||
<ul className="truncate">
|
<ul className="truncate">
|
||||||
{mostListenedArtists.map((artist, index) => (
|
{mostListenedArtists.map((artist, index) => (
|
||||||
<li key={index} className="w-full h-6 flex items-center justify-between overflow-hidden hover:underline">
|
<li key={index} className="w-full h-6 flex items-center justify-between overflow-hidden hover:underline">
|
||||||
<div className="text-left truncate">
|
<div className="text-left truncate">
|
||||||
{index + 1}. {artist.master_metadata_album_artist_name}
|
{index + 1}. {artist.master_metadata_album_artist_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right flex-shrink-0 ml-2">
|
<div className="text-right flex-shrink-0 ml-2">
|
||||||
{formatSeconds(artist.ms_played / 1000)}
|
{formatSeconds(artist.ms_played / 1000)}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
<RangeSlider
|
||||||
|
min={0}
|
||||||
<RangeSlider
|
max={getDaysBetween(firstStreamDate, lastStreamDate)}
|
||||||
min={0}
|
step={1}
|
||||||
max={getDaysBetween(firstStreamDate, lastStreamDate)}
|
|
||||||
step={1}
|
|
||||||
defaultValue={[0, getDaysBetween(firstStreamDate, lastStreamDate)]} // This bad boy right here
|
defaultValue={[0, getDaysBetween(firstStreamDate, lastStreamDate)]} // This bad boy right here
|
||||||
onInput={(value) => {
|
onInput={(value) => {
|
||||||
const startDate = getDaysAfter(firstStreamDate, value[0]);
|
const startDate = getDaysAfter(firstStreamDate, value[0]);
|
||||||
const endDate = getDaysAfter(firstStreamDate, value[1]);
|
const endDate = getDaysAfter(firstStreamDate, value[1]);
|
||||||
setDateRange([startDate, endDate]);
|
setDateRange([startDate, endDate]);
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={dateRange[0].toISOString().split('T')[0]}
|
value={dateRange[0].toISOString().split("T")[0]}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (new Date(e.target.value) > dateRange[1]) {
|
if (new Date(e.target.value) > dateRange[1]) {
|
||||||
alert('Start date cannot be after end date');
|
alert("Start date cannot be after end date");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if valid date
|
||||||
|
if (isNaN(new Date(e.target.value).getTime())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// check if valid date
|
setDateRange([new Date(e.target.value), dateRange[1]]);
|
||||||
if (isNaN(new Date(e.target.value).getTime())) {
|
}}
|
||||||
return;
|
className="mr-2"
|
||||||
}
|
/>
|
||||||
|
<input
|
||||||
setDateRange([new Date(e.target.value), dateRange[1]])
|
type="date"
|
||||||
}}
|
value={dateRange[1].toISOString().split("T")[0]}
|
||||||
className="mr-2"
|
onChange={(e) => {
|
||||||
/>
|
if (new Date(e.target.value) < dateRange[0]) {
|
||||||
<input
|
alert("End date cannot be before start date");
|
||||||
type="date"
|
return;
|
||||||
value={dateRange[1].toISOString().split('T')[0]}
|
}
|
||||||
onChange={(e) => {
|
|
||||||
if (new Date(e.target.value) < dateRange[0]) {
|
|
||||||
alert('End date cannot be before start date');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if valid date
|
// check if valid date
|
||||||
if (isNaN(new Date(e.target.value).getTime())) {
|
if (isNaN(new Date(e.target.value).getTime())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDateRange([dateRange[0], new Date(e.target.value)]);
|
setDateRange([dateRange[0], new Date(e.target.value)]);
|
||||||
}}
|
}}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p>({getDaysBetween(dateRange[0], dateRange[1])} days)</p>
|
<p>({getDaysBetween(dateRange[0], dateRange[1])} days)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default StatView;
|
export default StatView;
|
||||||
|
|||||||
Reference in New Issue
Block a user