Compare commits

..

No commits in common. '1bceeeb2f615ebc52066121a5f5234096bd0c06b' and 'fed2c5b95684964481539216fa7aa2ed8f830dba' have entirely different histories.

@ -16,65 +16,60 @@ interface getActivityArgs {
track_id: number;
}
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(
function getLastListens(
args: getItemsArgs
): Promise<PaginatedResponse<Listen>> {
const r = await fetch(
return 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}`
);
return handleJson<PaginatedResponse<Listen>>(r);
).then((r) => r.json() as Promise<PaginatedResponse<Listen>>);
}
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 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 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 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 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 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 getActivity(
args: getActivityArgs
): Promise<ListenActivityItem[]> {
const r = await fetch(
function getActivity(args: getActivityArgs): Promise<ListenActivityItem[]> {
return 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}`
);
return handleJson<ListenActivityItem[]>(r);
).then((r) => r.json() as Promise<ListenActivityItem[]>);
}
async function getStats(period: string): Promise<Stats> {
const r = await fetch(`/apis/web/v1/stats?period=${period}`);
return handleJson<Stats>(r);
function getStats(period: string): Promise<Stats> {
return fetch(`/apis/web/v1/stats?period=${period}`).then(
(r) => r.json() as Promise<Stats>
);
}
function search(q: string): Promise<SearchResponse> {
@ -421,5 +416,4 @@ export type {
ApiError,
Config,
NowPlaying,
Stats,
};

@ -73,14 +73,8 @@ 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,11 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import { getStats, type Stats, type ApiError } from "api/api";
import { useQuery } from "@tanstack/react-query"
import { getStats } 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 (
@ -13,44 +14,32 @@ export default function AllTimeStats() {
<h2>All Time Stats</h2>
<p>Loading...</p>
</div>
);
} else if (isError) {
return (
<>
<div>
<h2>All Time Stats</h2>
<p className="error">Error: {error.message}</p>
</div>
</>
);
)
}
if (isError) {
return <p className="error">Error:{error.message}</p>
}
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={Math.floor(data.minutes_listened / 60) + " hours"}
>
{data.minutes_listened}
</span>{" "}
Minutes Listened
<span className={numberClasses} title={data.minutes_listened + " minutes"}>{Math.floor(data.minutes_listened / 60)}</span> Hours Listened
</div>
<div>
<span className={numberClasses}>{data.listen_count}</span> Plays
</div>
<div>
<span className={numberClasses}>{data.track_count}</span> Tracks
<span className={numberClasses}>{data.artist_count}</span> Artists
</div>
<div>
<span className={numberClasses}>{data.album_count}</span> Albums
</div>
<div>
<span className={numberClasses}>{data.artist_count}</span> Artists
<span className={numberClasses}>{data.track_count}</span> Tracks
</div>
</div>
);
)
}

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

@ -1,11 +1,5 @@
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";
@ -14,24 +8,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 }) => {
@ -42,67 +36,71 @@ 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));
r.json().then((r) => setError(r.error))
}
})
setLoading(false)
}
});
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));
r.json().then((r) => setError(r.error))
}
})
setLoading(false)
}
});
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));
r.json().then((r) => setError(r.error))
}
})
setLoading(false)
}
});
setLoading(false);
};
const handleClose = () => {
setOpen(false);
setInput("");
};
setOpen(false)
setInput('')
}
return (
<Modal maxW={1000} isOpen={open} onClose={handleClose}>
@ -112,24 +110,9 @@ 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">
@ -140,23 +123,18 @@ 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,48 +5,39 @@ 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 [error, setError] = useState("");
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 [suggestedImgLoading, setSuggestedImgLoading] = useState(true)
const doImageReplace = (url: string) => {
setLoading(true);
setError("");
const formData = new FormData();
formData.set(`${type.toLowerCase()}_id`, id.toString());
formData.set("image_url", url);
setLoading(true)
const formData = new FormData
formData.set(`${type.toLowerCase()}_id`, id.toString())
formData.set("image_url", url)
replaceImage(formData)
.then((r) => {
if (r.status >= 200 && r.status < 300) {
window.location.reload();
if (r.ok) {
window.location.reload()
} else {
r.json().then((r) => setError(r.error));
setLoading(false);
console.log(r)
setLoading(false)
}
})
.catch((err) => setError(err));
};
.catch((err) => console.log(err))
}
const closeModal = () => {
setOpen(false);
setQuery("");
setError("");
};
setOpen(false)
setQuery('')
}
return (
<Modal isOpen={open} onClose={closeModal}>
@ -56,34 +47,23 @@ export default function ImageReplaceModal({
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={`Enter image URL, or drag-and-drop a local file`}
placeholder={`Image URL`}
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 && (
@ -98,18 +78,13 @@ export default function ImageReplaceModal({
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,13 +19,12 @@ 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}
@ -33,35 +32,20 @@ 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 / 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>
}
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>}
>
<div className="mt-10">
<PeriodSelector setter={setPeriod} current={period} />

@ -20,18 +20,17 @@ 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}
@ -39,35 +38,20 @@ 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 / 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>
}
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>}
>
<div className="mt-10">
<PeriodSelector setter={setPeriod} current={period} />

@ -13,23 +13,20 @@ 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}
@ -37,44 +34,29 @@ 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 / 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>
}
{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>}
>
<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,57 +1,55 @@
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);
@ -67,25 +65,16 @@ 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;
}
@ -93,16 +82,21 @@ 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 "";
let minutes = Math.floor(seconds / 60);
return `${minutes} minutes listened`;
};
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`
}
}
export { hexToHSL, timeListenedString };
export type { hsl };
export {hexToHSL, timeListenedString}
export type {hsl}
Loading…
Cancel
Save