Compare commits

...

5 Commits

@ -16,60 +16,65 @@ interface getActivityArgs {
track_id: number;
}
function getLastListens(
async function handleJson<T>(r: Response): Promise<T> {
if (!r.ok) {
const err = await r.json();
throw Error(err.error);
}
return (await r.json()) as T;
}
async function getLastListens(
args: getItemsArgs
): Promise<PaginatedResponse<Listen>> {
return fetch(
const r = await fetch(
`/apis/web/v1/listens?period=${args.period}&limit=${args.limit}&artist_id=${args.artist_id}&album_id=${args.album_id}&track_id=${args.track_id}&page=${args.page}`
).then((r) => r.json() as Promise<PaginatedResponse<Listen>>);
);
return handleJson<PaginatedResponse<Listen>>(r);
}
function getTopTracks(args: getItemsArgs): Promise<PaginatedResponse<Track>> {
if (args.artist_id) {
return fetch(
`/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&artist_id=${args.artist_id}&page=${args.page}`
).then((r) => r.json() as Promise<PaginatedResponse<Track>>);
} else if (args.album_id) {
return fetch(
`/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&album_id=${args.album_id}&page=${args.page}`
).then((r) => r.json() as Promise<PaginatedResponse<Track>>);
} else {
return fetch(
`/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&page=${args.page}`
).then((r) => r.json() as Promise<PaginatedResponse<Track>>);
}
async function getTopTracks(
args: getItemsArgs
): Promise<PaginatedResponse<Track>> {
let url = `/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&page=${args.page}`;
if (args.artist_id) url += `&artist_id=${args.artist_id}`;
else if (args.album_id) url += `&album_id=${args.album_id}`;
const r = await fetch(url);
return handleJson<PaginatedResponse<Track>>(r);
}
function getTopAlbums(args: getItemsArgs): Promise<PaginatedResponse<Album>> {
const baseUri = `/apis/web/v1/top-albums?period=${args.period}&limit=${args.limit}&page=${args.page}`;
if (args.artist_id) {
return fetch(baseUri + `&artist_id=${args.artist_id}`).then(
(r) => r.json() as Promise<PaginatedResponse<Album>>
);
} else {
return fetch(baseUri).then(
(r) => r.json() as Promise<PaginatedResponse<Album>>
);
}
async function getTopAlbums(
args: getItemsArgs
): Promise<PaginatedResponse<Album>> {
let url = `/apis/web/v1/top-albums?period=${args.period}&limit=${args.limit}&page=${args.page}`;
if (args.artist_id) url += `&artist_id=${args.artist_id}`;
const r = await fetch(url);
return handleJson<PaginatedResponse<Album>>(r);
}
function getTopArtists(args: getItemsArgs): Promise<PaginatedResponse<Artist>> {
const baseUri = `/apis/web/v1/top-artists?period=${args.period}&limit=${args.limit}&page=${args.page}`;
return fetch(baseUri).then(
(r) => r.json() as Promise<PaginatedResponse<Artist>>
);
async function getTopArtists(
args: getItemsArgs
): Promise<PaginatedResponse<Artist>> {
const url = `/apis/web/v1/top-artists?period=${args.period}&limit=${args.limit}&page=${args.page}`;
const r = await fetch(url);
return handleJson<PaginatedResponse<Artist>>(r);
}
function getActivity(args: getActivityArgs): Promise<ListenActivityItem[]> {
return fetch(
async function getActivity(
args: getActivityArgs
): Promise<ListenActivityItem[]> {
const r = await fetch(
`/apis/web/v1/listen-activity?step=${args.step}&range=${args.range}&month=${args.month}&year=${args.year}&album_id=${args.album_id}&artist_id=${args.artist_id}&track_id=${args.track_id}`
).then((r) => r.json() as Promise<ListenActivityItem[]>);
);
return handleJson<ListenActivityItem[]>(r);
}
function getStats(period: string): Promise<Stats> {
return fetch(`/apis/web/v1/stats?period=${period}`).then(
(r) => r.json() as Promise<Stats>
);
async function getStats(period: string): Promise<Stats> {
const r = await fetch(`/apis/web/v1/stats?period=${period}`);
return handleJson<Stats>(r);
}
function search(q: string): Promise<SearchResponse> {
@ -416,4 +421,5 @@ export type {
ApiError,
Config,
NowPlaying,
Stats,
};

@ -73,8 +73,14 @@ export default function ActivityGrid({
<p>Loading...</p>
</div>
);
} else if (isError) {
return (
<div className="w-[500px]">
<h2>Activity</h2>
<p className="error">Error: {error.message}</p>
</div>
);
}
if (isError) return <p className="error">Error:{error.message}</p>;
// from https://css-tricks.com/snippets/javascript/lighten-darken-color/
function LightenDarkenColor(hex: string, lum: number) {

@ -1,12 +1,11 @@
import { useQuery } from "@tanstack/react-query"
import { getStats } from "api/api"
import { useQuery } from "@tanstack/react-query";
import { getStats, type Stats, type ApiError } from "api/api";
export default function AllTimeStats() {
const { isPending, isError, data, error } = useQuery({
queryKey: ['stats', 'all_time'],
queryKey: ["stats", "all_time"],
queryFn: ({ queryKey }) => getStats(queryKey[1]),
})
});
if (isPending) {
return (
@ -14,32 +13,44 @@ export default function AllTimeStats() {
<h2>All Time Stats</h2>
<p>Loading...</p>
</div>
)
}
if (isError) {
return <p className="error">Error:{error.message}</p>
);
} else if (isError) {
return (
<>
<div>
<h2>All Time Stats</h2>
<p className="error">Error: {error.message}</p>
</div>
</>
);
}
const numberClasses = 'header-font font-bold text-xl'
const numberClasses = "header-font font-bold text-xl";
return (
<div>
<h2>All Time Stats</h2>
<div>
<span className={numberClasses} title={data.minutes_listened + " minutes"}>{Math.floor(data.minutes_listened / 60)}</span> Hours Listened
<span
className={numberClasses}
title={Math.floor(data.minutes_listened / 60) + " hours"}
>
{data.minutes_listened}
</span>{" "}
Minutes Listened
</div>
<div>
<span className={numberClasses}>{data.listen_count}</span> Plays
</div>
<div>
<span className={numberClasses}>{data.artist_count}</span> Artists
<span className={numberClasses}>{data.track_count}</span> Tracks
</div>
<div>
<span className={numberClasses}>{data.album_count}</span> Albums
</div>
<div>
<span className={numberClasses}>{data.track_count}</span> Tracks
<span className={numberClasses}>{data.artist_count}</span> Artists
</div>
</div>
)
);
}

@ -67,12 +67,14 @@ export default function LastPlays(props: Props) {
<p>Loading...</p>
</div>
);
} else if (isError) {
return (
<div className="w-[300px] sm:w-[500px]">
<h2>Last Played</h2>
<p className="error">Error: {error.message}</p>
</div>
);
}
if (isError) {
return <p className="error">Error: {error.message}</p>;
}
if (!data.items) return;
const listens = items ?? data.items;

@ -37,11 +37,14 @@ export default function TopAlbums(props: Props) {
<p>Loading...</p>
</div>
);
} else if (isError) {
return (
<div className="w-[300px]">
<h2>Top Albums</h2>
<p className="error">Error: {error.message}</p>
</div>
);
}
if (isError) {
return <p className="error">Error:{error.message}</p>;
}
if (!data.items) return;
return (
<div>

@ -28,11 +28,14 @@ export default function TopArtists(props: Props) {
<p>Loading...</p>
</div>
);
} else if (isError) {
return (
<div className="w-[300px]">
<h2>Top Artists</h2>
<p className="error">Error: {error.message}</p>
</div>
);
}
if (isError) {
return <p className="error">Error:{error.message}</p>;
}
if (!data.items) return;
return (
<div>

@ -35,9 +35,13 @@ const TopTracks = (props: Props) => {
<p>Loading...</p>
</div>
);
}
if (isError) {
return <p className="error">Error:{error.message}</p>;
} else if (isError) {
return (
<div className="w-[300px]">
<h2>Top Tracks</h2>
<p className="error">Error: {error.message}</p>
</div>
);
}
if (!data.items) return;

@ -1,5 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { createAlias, deleteAlias, getAliases, setPrimaryAlias, type Alias } from "api/api";
import {
createAlias,
deleteAlias,
getAliases,
setPrimaryAlias,
type Alias,
} from "api/api";
import { Modal } from "../Modal";
import { AsyncButton } from "../../AsyncButton";
import { useEffect, useState } from "react";
@ -8,24 +14,24 @@ import SetVariousArtists from "./SetVariousArtist";
import SetPrimaryArtist from "./SetPrimaryArtist";
interface Props {
type: string
id: number
open: boolean
setOpen: Function
type: string;
id: number;
open: boolean;
setOpen: Function;
}
export default function EditModal({ open, setOpen, type, id }: Props) {
const [input, setInput] = useState('')
const [loading, setLoading ] = useState(false)
const [err, setError ] = useState<string>()
const [displayData, setDisplayData] = useState<Alias[]>([])
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [err, setError] = useState<string>();
const [displayData, setDisplayData] = useState<Alias[]>([]);
const { isPending, isError, data, error } = useQuery({
queryKey: [
'aliases',
"aliases",
{
type: type,
id: id
id: id,
},
],
queryFn: ({ queryKey }) => {
@ -36,71 +42,67 @@ export default function EditModal({ open, setOpen, type, id }: Props) {
useEffect(() => {
if (data) {
setDisplayData(data)
setDisplayData(data);
}
}, [data])
}, [data]);
if (isError) {
return (
<p className="error">Error: {error.message}</p>
)
return <p className="error">Error: {error.message}</p>;
}
if (isPending) {
return (
<p>Loading...</p>
)
return <p>Loading...</p>;
}
const handleSetPrimary = (alias: string) => {
setError(undefined)
setLoading(true)
setPrimaryAlias(type, id, alias)
.then(r => {
setError(undefined);
setLoading(true);
setPrimaryAlias(type, id, alias).then((r) => {
if (r.ok) {
window.location.reload()
window.location.reload();
} else {
r.json().then((r) => setError(r.error))
}
})
setLoading(false)
r.json().then((r) => setError(r.error));
}
});
setLoading(false);
};
const handleNewAlias = () => {
setError(undefined)
setError(undefined);
if (input === "") {
setError("alias must be provided")
return
setError("alias must be provided");
return;
}
setLoading(true)
createAlias(type, id, input)
.then(r => {
setLoading(true);
createAlias(type, id, input).then((r) => {
if (r.ok) {
setDisplayData([...displayData, {alias: input, source: "Manual", is_primary: false, id: id}])
setDisplayData([
...displayData,
{ alias: input, source: "Manual", is_primary: false, id: id },
]);
} else {
r.json().then((r) => setError(r.error))
}
})
setLoading(false)
r.json().then((r) => setError(r.error));
}
});
setLoading(false);
};
const handleDeleteAlias = (alias: string) => {
setError(undefined)
setLoading(true)
deleteAlias(type, id, alias)
.then(r => {
setError(undefined);
setLoading(true);
deleteAlias(type, id, alias).then((r) => {
if (r.ok) {
setDisplayData(displayData.filter((v) => v.alias != alias))
setDisplayData(displayData.filter((v) => v.alias != alias));
} else {
r.json().then((r) => setError(r.error))
}
})
setLoading(false)
r.json().then((r) => setError(r.error));
}
});
setLoading(false);
};
const handleClose = () => {
setOpen(false)
setInput('')
}
setOpen(false);
setInput("");
};
return (
<Modal maxW={1000} isOpen={open} onClose={handleClose}>
@ -110,9 +112,24 @@ export default function EditModal({ open, setOpen, type, id }: Props) {
<div className="flex flex-col gap-4">
{displayData.map((v) => (
<div className="flex gap-2">
<div className="bg p-3 rounded-md flex-grow" key={v.alias}>{v.alias} (source: {v.source})</div>
<AsyncButton loading={loading} onClick={() => handleSetPrimary(v.alias)} disabled={v.is_primary}>Set Primary</AsyncButton>
<AsyncButton loading={loading} onClick={() => handleDeleteAlias(v.alias)} confirm disabled={v.is_primary}><Trash size={16} /></AsyncButton>
<div className="bg p-3 rounded-md flex-grow" key={v.alias}>
{v.alias} (source: {v.source})
</div>
<AsyncButton
loading={loading}
onClick={() => handleSetPrimary(v.alias)}
disabled={v.is_primary}
>
Set Primary
</AsyncButton>
<AsyncButton
loading={loading}
onClick={() => handleDeleteAlias(v.alias)}
confirm
disabled={v.is_primary}
>
<Trash size={16} />
</AsyncButton>
</div>
))}
<div className="flex gap-2 w-3/5">
@ -123,18 +140,23 @@ export default function EditModal({ open, setOpen, type, id }: Props) {
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<AsyncButton loading={loading} onClick={handleNewAlias}>Submit</AsyncButton>
<AsyncButton loading={loading} onClick={handleNewAlias}>
Submit
</AsyncButton>
</div>
{err && <p className="error">{err}</p>}
</div>
</div>
{ type.toLowerCase() === "album" &&
{type.toLowerCase() === "album" && (
<>
<SetVariousArtists id={id} />
<SetPrimaryArtist id={id} type="album" />
</>
}
)}
{type.toLowerCase() === "track" && (
<SetPrimaryArtist id={id} type="track" />
)}
</div>
</Modal>
)
);
}

@ -5,39 +5,48 @@ import SearchResults from "../SearchResults";
import { AsyncButton } from "../AsyncButton";
interface Props {
type: string
id: number
musicbrainzId?: string
open: boolean
setOpen: Function
type: string;
id: number;
musicbrainzId?: string;
open: boolean;
setOpen: Function;
}
export default function ImageReplaceModal({ musicbrainzId, type, id, open, setOpen }: Props) {
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(false)
const [suggestedImgLoading, setSuggestedImgLoading] = useState(true)
export default function ImageReplaceModal({
musicbrainzId,
type,
id,
open,
setOpen,
}: Props) {
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [suggestedImgLoading, setSuggestedImgLoading] = useState(true);
const doImageReplace = (url: string) => {
setLoading(true)
const formData = new FormData
formData.set(`${type.toLowerCase()}_id`, id.toString())
formData.set("image_url", url)
setLoading(true);
setError("");
const formData = new FormData();
formData.set(`${type.toLowerCase()}_id`, id.toString());
formData.set("image_url", url);
replaceImage(formData)
.then((r) => {
if (r.ok) {
window.location.reload()
if (r.status >= 200 && r.status < 300) {
window.location.reload();
} else {
console.log(r)
setLoading(false)
r.json().then((r) => setError(r.error));
setLoading(false);
}
})
.catch((err) => console.log(err))
}
.catch((err) => setError(err));
};
const closeModal = () => {
setOpen(false)
setQuery('')
}
setOpen(false);
setQuery("");
setError("");
};
return (
<Modal isOpen={open} onClose={closeModal}>
@ -47,23 +56,34 @@ export default function ImageReplaceModal({ musicbrainzId, type, id, open, setOp
type="text"
autoFocus
// i find my stupid a(n) logic to be a little silly so im leaving it in even if its not optimal
placeholder={`Image URL`}
placeholder={`Enter image URL, or drag-and-drop a local file`}
className="w-full mx-auto fg bg rounded p-2"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{ query != "" ?
{query != "" ? (
<div className="flex gap-2 mt-4">
<AsyncButton loading={loading} onClick={() => doImageReplace(query)}>Submit</AsyncButton>
</div> :
''}
{ type === "Album" && musicbrainzId ?
<AsyncButton
loading={loading}
onClick={() => doImageReplace(query)}
>
Submit
</AsyncButton>
</div>
) : (
""
)}
{type === "Album" && musicbrainzId ? (
<>
<h3 className="mt-5">Suggested Image (Click to Apply)</h3>
<button
className="mt-4"
disabled={loading}
onClick={() => doImageReplace(`https://coverartarchive.org/release/${musicbrainzId}/front`)}
onClick={() =>
doImageReplace(
`https://coverartarchive.org/release/${musicbrainzId}/front`
)
}
>
<div className={`relative`}>
{suggestedImgLoading && (
@ -78,13 +98,18 @@ export default function ImageReplaceModal({ musicbrainzId, type, id, open, setOp
src={`https://coverartarchive.org/release/${musicbrainzId}/front`}
onLoad={() => setSuggestedImgLoading(false)}
onError={() => setSuggestedImgLoading(false)}
className={`block w-[130px] h-auto ${suggestedImgLoading ? 'opacity-0' : 'opacity-100'} transition-opacity duration-300`} />
className={`block w-[130px] h-auto ${
suggestedImgLoading ? "opacity-0" : "opacity-100"
} transition-opacity duration-300`}
/>
</div>
</button>
</>
: ''
}
) : (
""
)}
<p className="error">{error}</p>
</div>
</Modal>
)
);
}

@ -19,12 +19,13 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
export default function Album() {
const album = useLoaderData() as Album;
const [period, setPeriod] = useState('week')
const [period, setPeriod] = useState("week");
console.log(album)
console.log(album);
return (
<MediaLayout type="Album"
<MediaLayout
type="Album"
title={album.title}
img={album.image}
id={album.id}
@ -32,20 +33,35 @@ export default function Album() {
imgItemId={album.id}
mergeFunc={mergeAlbums}
mergeCleanerFunc={(r, id) => {
r.artists = []
r.tracks = []
for (let i = 0; i < r.albums.length; i ++) {
r.artists = [];
r.tracks = [];
for (let i = 0; i < r.albums.length; i++) {
if (r.albums[i].id === id) {
delete r.albums[i]
delete r.albums[i];
}
}
return r
return r;
}}
subContent={<div className="flex flex-col gap-2 items-start">
{album.listen_count && <p>{album.listen_count} play{ album.listen_count > 1 ? 's' : ''}</p>}
{<p title={Math.floor(album.time_listened / 60) + " minutes"}>{timeListenedString(album.time_listened)}</p>}
{<p title={new Date(album.first_listen * 1000).toLocaleString()}>Listening since {new Date(album.first_listen * 1000).toLocaleDateString()}</p>}
</div>}
subContent={
<div className="flex flex-col gap-2 items-start">
{album.listen_count && (
<p>
{album.listen_count} play{album.listen_count > 1 ? "s" : ""}
</p>
)}
{
<p title={Math.floor(album.time_listened / 60 / 60) + " hours"}>
{timeListenedString(album.time_listened)}
</p>
}
{
<p title={new Date(album.first_listen * 1000).toLocaleString()}>
Listening since{" "}
{new Date(album.first_listen * 1000).toLocaleDateString()}
</p>
}
</div>
}
>
<div className="mt-10">
<PeriodSelector setter={setPeriod} current={period} />

@ -20,17 +20,18 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
export default function Artist() {
const artist = useLoaderData() as Artist;
const [period, setPeriod] = useState('week')
const [period, setPeriod] = useState("week");
// remove canonical name from alias list
console.log(artist.aliases)
console.log(artist.aliases);
let index = artist.aliases.indexOf(artist.name);
if (index !== -1) {
artist.aliases.splice(index, 1);
}
return (
<MediaLayout type="Artist"
<MediaLayout
type="Artist"
title={artist.name}
img={artist.image}
id={artist.id}
@ -38,20 +39,35 @@ export default function Artist() {
imgItemId={artist.id}
mergeFunc={mergeArtists}
mergeCleanerFunc={(r, id) => {
r.albums = []
r.tracks = []
for (let i = 0; i < r.artists.length; i ++) {
r.albums = [];
r.tracks = [];
for (let i = 0; i < r.artists.length; i++) {
if (r.artists[i].id === id) {
delete r.artists[i]
delete r.artists[i];
}
}
return r
return r;
}}
subContent={<div className="flex flex-col gap-2 items-start">
{artist.listen_count && <p>{artist.listen_count} play{ artist.listen_count > 1 ? 's' : ''}</p>}
{<p title={Math.floor(artist.time_listened / 60) + " minutes"}>{timeListenedString(artist.time_listened)}</p>}
{<p title={new Date(artist.first_listen * 1000).toLocaleString()}>Listening since {new Date(artist.first_listen * 1000).toLocaleDateString()}</p>}
</div>}
subContent={
<div className="flex flex-col gap-2 items-start">
{artist.listen_count && (
<p>
{artist.listen_count} play{artist.listen_count > 1 ? "s" : ""}
</p>
)}
{
<p title={Math.floor(artist.time_listened / 60 / 60) + " hours"}>
{timeListenedString(artist.time_listened)}
</p>
}
{
<p title={new Date(artist.first_listen * 1000).toLocaleString()}>
Listening since{" "}
{new Date(artist.first_listen * 1000).toLocaleDateString()}
</p>
}
</div>
}
>
<div className="mt-10">
<PeriodSelector setter={setPeriod} current={period} />

@ -13,20 +13,23 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
throw new Response("Failed to load track", { status: res.status });
}
const track: Track = await res.json();
res = await fetch(`/apis/web/v1/album?id=${track.album_id}`)
res = await fetch(`/apis/web/v1/album?id=${track.album_id}`);
if (!res.ok) {
throw new Response("Failed to load album for track", { status: res.status })
throw new Response("Failed to load album for track", {
status: res.status,
});
}
const album: Album = await res.json()
return {track: track, album: album};
const album: Album = await res.json();
return { track: track, album: album };
}
export default function Track() {
const { track, album } = useLoaderData();
const [period, setPeriod] = useState('week')
const [period, setPeriod] = useState("week");
return (
<MediaLayout type="Track"
<MediaLayout
type="Track"
title={track.title}
img={track.image}
id={track.id}
@ -34,29 +37,44 @@ export default function Track() {
imgItemId={track.album_id}
mergeFunc={mergeTracks}
mergeCleanerFunc={(r, id) => {
r.albums = []
r.artists = []
for (let i = 0; i < r.tracks.length; i ++) {
r.albums = [];
r.artists = [];
for (let i = 0; i < r.tracks.length; i++) {
if (r.tracks[i].id === id) {
delete r.tracks[i]
delete r.tracks[i];
}
}
return r
return r;
}}
subContent={<div className="flex flex-col gap-2 items-start">
subContent={
<div className="flex flex-col gap-2 items-start">
<Link to={`/album/${track.album_id}`}>appears on {album.title}</Link>
{track.listen_count && <p>{track.listen_count} play{ track.listen_count > 1 ? 's' : ''}</p>}
{<p title={Math.floor(track.time_listened / 60) + " minutes"}>{timeListenedString(track.time_listened)}</p>}
{<p title={new Date(track.first_listen * 1000).toLocaleString()}>Listening since {new Date(track.first_listen * 1000).toLocaleDateString()}</p>}
</div>}
{track.listen_count && (
<p>
{track.listen_count} play{track.listen_count > 1 ? "s" : ""}
</p>
)}
{
<p title={Math.floor(track.time_listened / 60 / 60) + " hours"}>
{timeListenedString(track.time_listened)}
</p>
}
{
<p title={new Date(track.first_listen * 1000).toLocaleString()}>
Listening since{" "}
{new Date(track.first_listen * 1000).toLocaleDateString()}
</p>
}
</div>
}
>
<div className="mt-10">
<PeriodSelector setter={setPeriod} current={period} />
</div>
<div className="flex flex-wrap gap-20 mt-10">
<LastPlays limit={20} trackId={track.id}/>
<LastPlays limit={20} trackId={track.id} />
<ActivityGrid trackId={track.id} configurable />
</div>
</MediaLayout>
)
);
}

@ -1,55 +1,57 @@
import Timeframe from "~/types/timeframe"
import Timeframe from "~/types/timeframe";
const timeframeToInterval = (timeframe: Timeframe): string => {
switch (timeframe) {
case Timeframe.Day:
return "1 day"
return "1 day";
case Timeframe.Week:
return "1 week"
return "1 week";
case Timeframe.Month:
return "1 month"
return "1 month";
case Timeframe.Year:
return "1 year"
return "1 year";
case Timeframe.AllTime:
return "99 years"
return "99 years";
}
}
};
function timeSince(date: Date) {
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const intervals = [
{ label: 'year', seconds: 31536000 },
{ label: 'month', seconds: 2592000 },
{ label: 'week', seconds: 604800 },
{ label: 'day', seconds: 86400 },
{ label: 'hour', seconds: 3600 },
{ label: 'minute', seconds: 60 },
{ label: 'second', seconds: 1 },
{ label: "year", seconds: 31536000 },
{ label: "month", seconds: 2592000 },
{ label: "week", seconds: 604800 },
{ label: "day", seconds: 86400 },
{ label: "hour", seconds: 3600 },
{ label: "minute", seconds: 60 },
{ label: "second", seconds: 1 },
];
for (const interval of intervals) {
const count = Math.floor(seconds / interval.seconds);
if (count >= 1) {
return `${count} ${interval.label}${count !== 1 ? 's' : ''} ago`;
return `${count} ${interval.label}${count !== 1 ? "s" : ""} ago`;
}
}
return 'just now';
return "just now";
}
export { timeSince }
export { timeSince };
type hsl = {
h: number,
s: number,
l: number,
}
h: number;
s: number;
l: number;
};
const hexToHSL = (hex: string): hsl => {
let r = 0, g = 0, b = 0;
hex = hex.replace('#', '');
let r = 0,
g = 0,
b = 0;
hex = hex.replace("#", "");
if (hex.length === 3) {
r = parseInt(hex[0] + hex[0], 16);
@ -65,16 +67,25 @@ const hexToHSL = (hex: string): hsl => {
g /= 255;
b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
const max = Math.max(r, g, b),
min = Math.min(r, g, b);
let h = 0,
s = 0,
l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)); break;
case g: h = ((b - r) / d + 2); break;
case b: h = ((r - g) / d + 4); break;
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
@ -82,21 +93,16 @@ const hexToHSL = (hex: string): hsl => {
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
l: Math.round(l * 100),
};
};
const timeListenedString = (seconds: number) => {
if (!seconds) return ""
if (seconds > (120 * 60) - 1) {
let hours = Math.floor(seconds / 60 / 60)
return `${hours} hours listened`
} else {
let minutes = Math.floor(seconds / 60)
return `${minutes} minutes listened`
}
}
if (!seconds) return "";
let minutes = Math.floor(seconds / 60);
return `${minutes} minutes listened`;
};
export {hexToHSL, timeListenedString}
export type {hsl}
export { hexToHSL, timeListenedString };
export type { hsl };

Loading…
Cancel
Save