diff --git a/apps/web/src/api/library.ts b/apps/web/src/api/library.ts index b467087a..53d4293b 100644 --- a/apps/web/src/api/library.ts +++ b/apps/web/src/api/library.ts @@ -70,23 +70,59 @@ export const getArtistAlbums = async ( return response.data.albums; }; -export const getArtists = async (did: string, offset = 0, limit = 30) => { +export const getArtists = async ( + did: string, + offset = 0, + limit = 30, + startDate?: Date, + endDate?: Date, +) => { const response = await client.get("/xrpc/app.rocksky.actor.getActorArtists", { - params: { did, limit, offset }, + params: { + did, + limit, + offset, + startDate: startDate?.toISOString(), + endDate: endDate?.toISOString(), + }, }); return response.data; }; -export const getAlbums = async (did: string, offset = 0, limit = 12) => { +export const getAlbums = async ( + did: string, + offset = 0, + limit = 12, + startDate?: Date, + endDate?: Date, +) => { const response = await client.get("/xrpc/app.rocksky.actor.getActorAlbums", { - params: { did, limit, offset }, + params: { + did, + limit, + offset, + startDate: startDate?.toISOString(), + endDate: endDate?.toISOString(), + }, }); return response.data; }; -export const getTracks = async (did: string, offset = 0, limit = 20) => { +export const getTracks = async ( + did: string, + offset = 0, + limit = 20, + startDate?: Date, + endDate?: Date, +) => { const response = await client.get("/xrpc/app.rocksky.actor.getActorSongs", { - params: { did, limit, offset }, + params: { + did, + limit, + offset, + startDate: startDate?.toISOString(), + endDate: endDate?.toISOString(), + }, }); return response.data; }; diff --git a/apps/web/src/components/LastDaysMenu/LastDaysMenu.tsx b/apps/web/src/components/LastDaysMenu/LastDaysMenu.tsx new file mode 100644 index 00000000..9897f8ec --- /dev/null +++ b/apps/web/src/components/LastDaysMenu/LastDaysMenu.tsx @@ -0,0 +1,137 @@ +import { NestedMenus, StatefulMenu } from "baseui/menu"; +import { StatefulPopover } from "baseui/popover"; +import { + ALL_TIME, + LAST_180_DAYS, + LAST_30_DAYS, + LAST_365_DAYS, + LAST_7_DAYS, + LAST_90_DAYS, +} from "../../consts"; + +export interface LastDaysMenuProps { + children: React.ReactNode; + onSelect: (id: string) => void; +} + +function LastDaysMenu(props: LastDaysMenuProps) { + return ( + ( +
+ + { + props.onSelect(item.id); + close(); + }} + overrides={{ + List: { + style: { + boxShadow: "none", + outline: "none !important", + backgroundColor: "var(--color-background)", + }, + }, + ListItem: { + style: { + backgroundColor: "var(--color-background)", + color: "var(--color-text)", + ":hover": { + backgroundColor: "var(--color-menu-hover)", + }, + }, + }, + Option: { + props: { + getChildMenu: (item: { label: string }) => { + if (item.label === "Add to Playlist") { + return ( +
+ +
+ ); + } + return null; + }, + }, + }, + }} + /> +
+
+ )} + overrides={{ + Arrow: { + style: { + backgroundColor: "var(--color-border)", + }, + }, + Inner: { + style: { + backgroundColor: "var(--color-background)", + }, + }, + }} + > + {props.children} +
+ ); +} + +export default LastDaysMenu; diff --git a/apps/web/src/components/LastDaysMenu/index.tsx b/apps/web/src/components/LastDaysMenu/index.tsx new file mode 100644 index 00000000..cd99c231 --- /dev/null +++ b/apps/web/src/components/LastDaysMenu/index.tsx @@ -0,0 +1,3 @@ +import LastDaysMenu from "./LastDaysMenu"; + +export default LastDaysMenu; diff --git a/apps/web/src/consts.ts b/apps/web/src/consts.ts index 3e41e29c..075f37fd 100644 --- a/apps/web/src/consts.ts +++ b/apps/web/src/consts.ts @@ -18,3 +18,19 @@ export const AUDIO_EXTENSIONS = [ "opus", "wma", ]; + +export const LAST_7_DAYS = "LAST_7_DAYS"; +export const LAST_30_DAYS = "LAST_30_DAYS"; +export const LAST_90_DAYS = "LAST_90_DAYS"; +export const LAST_180_DAYS = "LAST_180_DAYS"; +export const LAST_365_DAYS = "LAST_365_DAYS"; +export const ALL_TIME = "ALL_TIME"; + +export const LAST_DAYS_LABELS: Record = { + [LAST_7_DAYS]: "Last 7 days", + [LAST_30_DAYS]: "Last 30 days", + [LAST_90_DAYS]: "Last 90 days", + [LAST_180_DAYS]: "Last 180 days", + [LAST_365_DAYS]: "Last 365 days", + [ALL_TIME]: "All Time", +}; diff --git a/apps/web/src/hooks/useLibrary.tsx b/apps/web/src/hooks/useLibrary.tsx index c107ba36..c76ef85d 100644 --- a/apps/web/src/hooks/useLibrary.tsx +++ b/apps/web/src/hooks/useLibrary.tsx @@ -33,10 +33,16 @@ export const useArtistAlbumsQuery = (uri: string, limit = 10) => enabled: !!uri, }); -export const useArtistsQuery = (did: string, offset = 0, limit = 30) => +export const useArtistsQuery = ( + did: string, + offset = 0, + limit = 30, + startDate?: Date, + endDate?: Date, +) => useQuery({ - queryKey: ["artists", did, offset, limit], - queryFn: () => getArtists(did, offset, limit), + queryKey: ["artists", did, offset, limit, startDate, endDate], + queryFn: () => getArtists(did, offset, limit, startDate, endDate), enabled: !!did, select: (data) => // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -46,10 +52,16 @@ export const useArtistsQuery = (did: string, offset = 0, limit = 30) => })), }); -export const useAlbumsQuery = (did: string, offset = 0, limit = 12) => +export const useAlbumsQuery = ( + did: string, + offset = 0, + limit = 12, + startDate?: Date, + endDate?: Date, +) => useQuery({ - queryKey: ["albums", did, offset, limit], - queryFn: () => getAlbums(did, offset, limit), + queryKey: ["albums", did, offset, limit, startDate, endDate], + queryFn: () => getAlbums(did, offset, limit, startDate, endDate), enabled: !!did, select: (data) => // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -59,10 +71,16 @@ export const useAlbumsQuery = (did: string, offset = 0, limit = 12) => })), }); -export const useTracksQuery = (did: string, offset = 0, limit = 20) => +export const useTracksQuery = ( + did: string, + offset = 0, + limit = 20, + startDate?: Date, + endDate?: Date, +) => useQuery({ - queryKey: ["tracks", did, offset, limit], - queryFn: () => getTracks(did, offset, limit), + queryKey: ["tracks", did, offset, limit, startDate, endDate], + queryFn: () => getTracks(did, offset, limit, startDate, endDate), enabled: !!did, select: (data) => // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/web/src/lib/date.ts b/apps/web/src/lib/date.ts new file mode 100644 index 00000000..34d8c0f7 --- /dev/null +++ b/apps/web/src/lib/date.ts @@ -0,0 +1,25 @@ +import dayjs from "dayjs"; + +export const getLastDays = (days: number): [Date, Date] => { + const start = dayjs().subtract(days, "day").startOf("day").toDate(); + const end = dayjs().endOf("day").toDate(); + return [start, end]; +}; + +export const getLastWeek = (): [Date, Date] => { + const start = dayjs().subtract(1, "week").startOf("week").toDate(); + const end = dayjs().subtract(1, "week").endOf("week").toDate(); + return [start, end]; +}; + +export const getLastMonth = (): [Date, Date] => { + const start = dayjs().subtract(1, "month").startOf("month").toDate(); + const end = dayjs().subtract(1, "month").endOf("month").toDate(); + return [start, end]; +}; + +export const getLastYear = (): [Date, Date] => { + const start = dayjs().subtract(1, "year").startOf("year").toDate(); + const end = dayjs().subtract(1, "year").endOf("year").toDate(); + return [start, end]; +}; diff --git a/apps/web/src/pages/profile/overview/Overview.tsx b/apps/web/src/pages/profile/overview/Overview.tsx index 035d2a75..02ab5ff6 100644 --- a/apps/web/src/pages/profile/overview/Overview.tsx +++ b/apps/web/src/pages/profile/overview/Overview.tsx @@ -50,13 +50,13 @@ function Overview() {
- +
- +
); diff --git a/apps/web/src/pages/profile/overview/recenttracks/RecentTracks.tsx b/apps/web/src/pages/profile/overview/recenttracks/RecentTracks.tsx index 5e1349a7..6fdc2e71 100644 --- a/apps/web/src/pages/profile/overview/recenttracks/RecentTracks.tsx +++ b/apps/web/src/pages/profile/overview/recenttracks/RecentTracks.tsx @@ -128,7 +128,8 @@ function RecentTracks(props: RecentTracksProps) { See All diff --git a/apps/web/src/pages/profile/overview/topalbums/TopAlbums.tsx b/apps/web/src/pages/profile/overview/topalbums/TopAlbums.tsx index 3aaf79c2..c86706a7 100644 --- a/apps/web/src/pages/profile/overview/topalbums/TopAlbums.tsx +++ b/apps/web/src/pages/profile/overview/topalbums/TopAlbums.tsx @@ -4,11 +4,23 @@ import { BlockProps } from "baseui/block"; import { FlexGrid, FlexGridItem } from "baseui/flex-grid"; import { HeadingSmall, LabelMedium, LabelSmall } from "baseui/typography"; import { useAtomValue, useSetAtom } from "jotai"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { topAlbumsAtom } from "../../../../atoms/topAlbums"; import { userAtom } from "../../../../atoms/user"; import SongCover from "../../../../components/SongCover"; import { useAlbumsQuery } from "../../../../hooks/useLibrary"; +import { IconChevronDown } from "@tabler/icons-react"; +import { getLastDays } from "../../../../lib/date"; +import { + ALL_TIME, + LAST_180_DAYS, + LAST_30_DAYS, + LAST_365_DAYS, + LAST_7_DAYS, + LAST_90_DAYS, + LAST_DAYS_LABELS, +} from "../../../../consts"; +import LastDaysMenu from "../../../../components/LastDaysMenu"; const itemProps: BlockProps = { display: "flex", @@ -25,10 +37,12 @@ const Link = styled(DefaultLink)` `; function TopAlbums() { + const [topAlbumsRange, setTopAlbumsRange] = useState(LAST_7_DAYS); const setTopAlbums = useSetAtom(topAlbumsAtom); const topAlbums = useAtomValue(topAlbumsAtom); + const [range, setRange] = useState<[Date, Date] | []>(getLastDays(7)); const { did } = useParams({ strict: false }); - const albumsResult = useAlbumsQuery(did!); + const albumsResult = useAlbumsQuery(did!, 0, 12, ...range); const user = useAtomValue(userAtom); useEffect(() => { @@ -44,6 +58,32 @@ function TopAlbums() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [albumsResult.data, albumsResult.isLoading, albumsResult.isError, did]); + const onSelectLastDays = (id: string) => { + setTopAlbumsRange(id); + switch (id) { + case LAST_7_DAYS: + setRange(getLastDays(7)); + break; + case LAST_30_DAYS: + setRange(getLastDays(30)); + break; + case LAST_90_DAYS: + setRange(getLastDays(90)); + break; + case LAST_180_DAYS: + setRange(getLastDays(180)); + break; + case LAST_365_DAYS: + setRange(getLastDays(365)); + break; + case ALL_TIME: + setRange([]); + break; + default: + setRange([]); + } + }; + return ( <>
@@ -53,12 +93,17 @@ function TopAlbums() { > Top Albums - - See All - + + +
{!topAlbums.length && (
@@ -73,8 +118,8 @@ function TopAlbums() { > { // eslint-disable-next-line @typescript-eslint/no-explicit-any - topAlbums.map((album: any) => ( - + topAlbums.map((album: any, index: number) => ( + diff --git a/apps/web/src/pages/profile/overview/topartists/TopArtists.tsx b/apps/web/src/pages/profile/overview/topartists/TopArtists.tsx index a9c1bd9a..d1c9fc67 100644 --- a/apps/web/src/pages/profile/overview/topartists/TopArtists.tsx +++ b/apps/web/src/pages/profile/overview/topartists/TopArtists.tsx @@ -14,6 +14,17 @@ import Artist from "../../../../components/Icons/Artist"; import { useArtistsQuery } from "../../../../hooks/useLibrary"; import { useProfileStatsByDidQuery } from "../../../../hooks/useProfile"; import styles from "./styles"; +import { IconChevronDown } from "@tabler/icons-react"; +import { getLastDays } from "../../../../lib/date"; +import LastDaysMenu from "../../../../components/LastDaysMenu"; +import { + LAST_180_DAYS, + LAST_30_DAYS, + LAST_365_DAYS, + LAST_7_DAYS, + LAST_90_DAYS, + LAST_DAYS_LABELS, +} from "../../../../consts"; const Group = styled.div<{ mb?: number }>` display: flex; @@ -40,17 +51,29 @@ interface TopArtistsProps { offset?: number; size?: number; showPagination?: boolean; + withDateRange?: boolean; } function TopArtists(props: TopArtistsProps) { - const { showTitle = true, size = 30, showPagination } = props; + const { showTitle = true, size = 30, showPagination, withDateRange } = props; + const [topArtistsRange, setTopArtistsRange] = useState( + withDateRange ? LAST_7_DAYS : undefined, + ); + const [range, setRange] = useState<[Date, Date] | []>( + withDateRange ? getLastDays(7) : [], + ); const setTopArtists = useSetAtom(topArtistsAtom); const topArtists = useAtomValue(topArtistsAtom); const { darkMode } = useAtomValue(themeAtom); const { did } = useParams({ strict: false }); const profileStats = useProfileStatsByDidQuery(did!); const [currentPage, setCurrentPage] = useState(1); - const artistsResult = useArtistsQuery(did!, (currentPage - 1) * size, size); + const artistsResult = useArtistsQuery( + did!, + (currentPage - 1) * size, + size, + ...range, + ); const user = useAtomValue(userAtom); const pages = useMemo(() => { if (!did || !profileStats.data || !props.size) { @@ -59,6 +82,29 @@ function TopArtists(props: TopArtistsProps) { return Math.ceil(profileStats.data.artists / props.size) || 1; }, [profileStats.data, did, props.size]); + const onSelectLastDays = (id: string) => { + setTopArtistsRange(id); + switch (id) { + case LAST_7_DAYS: + setRange(getLastDays(7)); + break; + case LAST_30_DAYS: + setRange(getLastDays(30)); + break; + case LAST_90_DAYS: + setRange(getLastDays(90)); + break; + case LAST_180_DAYS: + setRange(getLastDays(180)); + break; + case LAST_365_DAYS: + setRange(getLastDays(365)); + break; + default: + setRange([]); + } + }; + useEffect(() => { if (artistsResult.isLoading || artistsResult.isError) { return; @@ -84,12 +130,17 @@ function TopArtists(props: TopArtistsProps) { > Top Artists - - See All - + + +
)} diff --git a/apps/web/src/pages/profile/overview/toptracks/TopTracks.tsx b/apps/web/src/pages/profile/overview/toptracks/TopTracks.tsx index 7adc6361..b67ba0ad 100644 --- a/apps/web/src/pages/profile/overview/toptracks/TopTracks.tsx +++ b/apps/web/src/pages/profile/overview/toptracks/TopTracks.tsx @@ -13,6 +13,17 @@ import { userAtom } from "../../../../atoms/user"; import { useTracksQuery } from "../../../../hooks/useLibrary"; import { useProfileStatsByDidQuery } from "../../../../hooks/useProfile"; import styles from "./styles"; +import { IconChevronDown } from "@tabler/icons-react"; +import { getLastDays } from "../../../../lib/date"; +import { + LAST_7_DAYS, + LAST_30_DAYS, + LAST_90_DAYS, + LAST_180_DAYS, + LAST_365_DAYS, + LAST_DAYS_LABELS, +} from "../../../../consts"; +import LastDaysMenu from "../../../../components/LastDaysMenu"; type Row = { id: string; @@ -51,6 +62,7 @@ interface TopTracksProps { offset?: number; size?: number; showPagination?: boolean; + withDateRange?: boolean; } function TopTracks(props: TopTracksProps) { @@ -60,6 +72,11 @@ function TopTracks(props: TopTracksProps) { showPagination: false, ...props, }; + + const [range, setRange] = useState<[Date, Date] | []>( + props.withDateRange ? getLastDays(7) : [], + ); + const [topTracksRange, setTopTracksRange] = useState(LAST_7_DAYS); const setTopTracks = useSetAtom(topTracksAtom); const topTracks = useAtomValue(topTracksAtom); const { darkMode } = useAtomValue(themeAtom); @@ -70,6 +87,7 @@ function TopTracks(props: TopTracksProps) { did!, (currentPage - 1) * props.size!, props.size!, + ...range, ); const user = useAtomValue(userAtom); const pages = useMemo(() => { @@ -92,6 +110,29 @@ function TopTracks(props: TopTracksProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [tracksResult.data, tracksResult.isLoading, tracksResult.isError, did]); + const onSelectLastDays = (id: string) => { + setTopTracksRange(id); + switch (id) { + case LAST_7_DAYS: + setRange(getLastDays(7)); + break; + case LAST_30_DAYS: + setRange(getLastDays(30)); + break; + case LAST_90_DAYS: + setRange(getLastDays(90)); + break; + case LAST_180_DAYS: + setRange(getLastDays(180)); + break; + case LAST_365_DAYS: + setRange(getLastDays(365)); + break; + default: + setRange([]); + } + }; + const maxScrobbles = topTracks.length > 0 ? topTracks[0].scrobbles || 1 : 0; return ( @@ -104,12 +145,17 @@ function TopTracks(props: TopTracksProps) { > Top Tracks - - See All - + + + )}