From 8deb8600f26d990cd6adf2ce8a7619bccd9907c9 Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 16 Oct 2023 10:43:58 -0400 Subject: [PATCH 01/10] Add search page (WIP) --- src/app/(main)/(secondary)/search/page.tsx | 13 +++ src/app/(main)/NavBar.tsx | 16 +++- src/components/Search.tsx | 102 +++++++++++++++++++++ src/lib/useDebouncedEffect.ts | 19 ++++ src/lib/utils.ts | 13 +++ src/styles/globals.css | 24 +++++ src/types.ts | 24 +++++ 7 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 src/app/(main)/(secondary)/search/page.tsx create mode 100644 src/components/Search.tsx create mode 100644 src/lib/useDebouncedEffect.ts diff --git a/src/app/(main)/(secondary)/search/page.tsx b/src/app/(main)/(secondary)/search/page.tsx new file mode 100644 index 0000000..912c093 --- /dev/null +++ b/src/app/(main)/(secondary)/search/page.tsx @@ -0,0 +1,13 @@ +import Search from '@/components/Search'; + +export default async function Page() { + return ( +
+ +
+ ); +} + +export const metadata = { + title: 'Search', +}; diff --git a/src/app/(main)/NavBar.tsx b/src/app/(main)/NavBar.tsx index 86ec8ae..d2313af 100644 --- a/src/app/(main)/NavBar.tsx +++ b/src/app/(main)/NavBar.tsx @@ -9,11 +9,14 @@ import MainNavHeader from './MainNavHeader'; export default async function NavBar() { const artists = await fetchArtists(); - const artistSlugsToName = artists.reduce((memo, next) => { - memo[String(next.slug)] = next.name; + const artistSlugsToName = artists.reduce( + (memo, next) => { + memo[String(next.slug)] = next.name; - return memo; - }, {} as Record); + return memo; + }, + {} as Record + ); return (
@@ -29,6 +32,11 @@ export default async function NavBar() {
+
+ + SEARCH + +
TIH diff --git a/src/components/Search.tsx b/src/components/Search.tsx new file mode 100644 index 0000000..aa88ad8 --- /dev/null +++ b/src/components/Search.tsx @@ -0,0 +1,102 @@ +'use client'; + +import { useState } from 'react'; +import useDebouncedEffect from '../lib/useDebouncedEffect'; +import { API_DOMAIN } from '../lib/constants'; +import { SearchResuts } from '../types'; +import Column from './Column'; +import Row from './Row'; + +async function fetchResults(query) { + const data: SearchResuts = await fetch(`${API_DOMAIN}/api/v2/search?q=${query}`, { + cache: 'no-cache', // seconds + }).then((res) => res.json()); + + return data; +} + +const Search = () => { + const [data, setData] = useState(null); + const [activeFilter, setActiveFilter] = useState('artists'); + const [searchValue, setSearchValue] = useState(''); + + useDebouncedEffect( + () => { + // TODO: sanitize, trim ... + if (searchValue) { + fetchResults(searchValue).then(setData); + } else { + setData(null); + } + }, + 200, + [searchValue] + ); + + return ( +
+
+ + setSearchValue(e.target.value)} + /> + +
+
    +
  • + +
  • +
  • + +
  • +
+ + {activeFilter === 'artists' && + data?.Artists?.map(({ name, uuid, slug, show_count, source_count }) => { + return ( + +
+
{name}
+
+
+
ARTIST
+
+
+ ); + })} + {activeFilter === 'songs' && + data?.Songs?.map(({ name, uuid, slim_artist }) => ( + +
+
{name}
+
{slim_artist?.name}
+
+
SONG
+
+ ))} +
+
+ ); +}; + +export default Search; diff --git a/src/lib/useDebouncedEffect.ts b/src/lib/useDebouncedEffect.ts new file mode 100644 index 0000000..54a150c --- /dev/null +++ b/src/lib/useDebouncedEffect.ts @@ -0,0 +1,19 @@ +import { useEffect, useRef } from 'react'; + +export default function useDebouncedEffect(effect, delay, dependencies) { + const effectRef = useRef(effect); + + useEffect(() => { + effectRef.current = effect; + }, [effect]); + + useEffect(() => { + const handler = setTimeout(() => { + effectRef.current(); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [...dependencies, delay]); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 53e3bc1..4e0915f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -55,6 +55,19 @@ export const simplePluralize = (str: string, count = 0): string => { return `${count?.toLocaleString()} ${count === 1 ? str : str + 's'}`; }; +/** example input and output: + * + * [ { id: 1, category: 'A' }, { id: 2, category: 'B' }, { id: 3, category: 'A' } ] + * + * { + * A: [ + * { id: 1, category: 'A' }, + * { id: 3, category: 'A' } + * ], + * B: [ { id: 2, category: 'B' } ] + * } + * + */ export const groupBy = function (xs, key) { return xs.reduce((rv, x) => { (rv[x[key]] = rv[x[key]] || []).push(x); diff --git a/src/styles/globals.css b/src/styles/globals.css index a8f761e..39ac49a 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -21,6 +21,30 @@ .button { @apply rounded bg-green-400 p-4 py-2 border border-green-100 my-4 inline-block font-medium text-black/70 tracking-wide; } + + .search-bar { + @apply rounded-full border border-gray-400; + } + + .search-bar:focus-within { + @apply border-gray-600; + } + + .search-bar > input:focus { + outline: none; + } + + .search-filters > li { + @apply inline-block; + } + + .search-filters > li > button { + @apply rounded-full px-2 py-1; + } + + .search-filters > li > button.search-filters-button--active { + @apply bg-relisten-100 text-white; + } } body { margin: 0; font-family: Roboto, Helvetica, Helvetica Neue, sans-serif; -webkit-font-smoothing: antialiased; color: #333; } diff --git a/src/types.ts b/src/types.ts index ad9a63f..1bb00b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -337,6 +337,21 @@ export type Playback = { activeTrack?: ActiveTrack; }; +export type Song = { + artist_id?: number; + artist_uuid?: string; + created_at?: string; + id: number; + name?: string; + shows_played_at?: number; + slim_artist?: Artist; + slug?: string; + sortName?: string; + updated_at?: string; + upstream_identifier?: string; + uuid?: string; +}; + export type ActiveTrack = { currentTime?: number; duration?: number; @@ -345,3 +360,12 @@ export type ActiveTrack = { playbackType?: string; webAudioLoadingState?: string; }; + +export type SearchResuts = { + Artists: Artist[]; + Shows: Show[]; + Songs: Song[]; + Sources: Source[]; + Tours: Tour[]; + Venues: Venue[]; +}; From 45663576e28185dc3a04e469e91ecc7a024a2033 Mon Sep 17 00:00:00 2001 From: Jared Salzano Date: Fri, 5 Jan 2024 17:57:53 -0500 Subject: [PATCH 02/10] More initial work on search page --- src/components/Search.tsx | 214 ++++++++++++++++++++++--------- src/components/SearchResults.tsx | 173 +++++++++++++++++++++++++ src/lib/utils.ts | 19 +++ src/types.ts | 12 ++ 4 files changed, 355 insertions(+), 63 deletions(-) create mode 100644 src/components/SearchResults.tsx diff --git a/src/components/Search.tsx b/src/components/Search.tsx index aa88ad8..6eaa0f9 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,13 +1,15 @@ 'use client'; -import { useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import useDebouncedEffect from '../lib/useDebouncedEffect'; import { API_DOMAIN } from '../lib/constants'; -import { SearchResuts } from '../types'; +import SearchResults from './SearchResults'; +import { Artist, SearchResuts, SearchResultsType, SongVersions } from '../types'; import Column from './Column'; +import { SimplePopover } from './Popover'; import Row from './Row'; -async function fetchResults(query) { +async function fetchSearchResults(query: string) { const data: SearchResuts = await fetch(`${API_DOMAIN}/api/v2/search?q=${query}`, { cache: 'no-cache', // seconds }).then((res) => res.json()); @@ -15,16 +17,55 @@ async function fetchResults(query) { return data; } +const SortByMenu = ({ + resultsType, + setSortBy, +}: { + resultsType: SearchResultsType; + setSortBy: (string) => void; +}) => { + if (resultsType === 'all') { + return null; + } + + if (resultsType === 'artists') { + return null; + } + + if (resultsType === 'songs') { + return null; + } + + if (resultsType === 'versions') { + return ( + + + + + ); + } +}; + const Search = () => { const [data, setData] = useState(null); - const [activeFilter, setActiveFilter] = useState('artists'); + const [versionsData, setVersionsData] = useState(null); + const [resultsType, setResultsType] = useState('all'); const [searchValue, setSearchValue] = useState(''); + const [sortBy, setSortBy] = useState('DATE_ASC'); useDebouncedEffect( () => { // TODO: sanitize, trim ... if (searchValue) { - fetchResults(searchValue).then(setData); + fetchSearchResults(searchValue).then((r) => { + // API sometimes returns null instead of [] + const songs = !r.Songs ? [] : r.Songs; + setData({ ...r, Songs: songs }); + }); } else { setData(null); } @@ -33,68 +74,115 @@ const Search = () => { [searchValue] ); + const clearSearch = useCallback(() => { + setSearchValue(''); + setData(null); + setResultsType('all'); + setVersionsData(null); + setSortBy(''); + }, []); + + const getSources = useCallback( + (slim_artist: Artist | undefined, songUuid: string | undefined) => { + if (!songUuid || !slim_artist || !slim_artist.slug) { + // TODO: handle error + return; + } + + fetch(`${API_DOMAIN}/api/v3/artists/${slim_artist.uuid}/songs/${songUuid}`, { + cache: 'no-cache', // seconds + }).then((res) => { + res.json().then((json) => { + setResultsType('versions'); + setVersionsData({ + ...json, + artistName: slim_artist.name, + artistSlug: slim_artist.slug, + }); + }); + }); + }, + [] + ); + + useEffect(() => { + console.log('data: ', data); + console.log('sortBy: ', sortBy); + console.log('versionsData: ', versionsData); + }, [sortBy, data, versionsData]); + return (
-
- - setSearchValue(e.target.value)} - /> - -
-
    -
  • - -
  • -
  • - -
  • -
- - {activeFilter === 'artists' && - data?.Artists?.map(({ name, uuid, slug, show_count, source_count }) => { - return ( - -
-
{name}
-
-
-
ARTIST
-
-
- ); - })} - {activeFilter === 'songs' && - data?.Songs?.map(({ name, uuid, slim_artist }) => ( - -
-
{name}
-
{slim_artist?.name}
-
-
SONG
-
- ))} -
+
+ )} +
+ {data !== null && resultsType === 'versions' && ( +
+ Showing all versions of “{versionsData?.name}” by {versionsData?.artistName} +
+ )} + {data !== null && resultsType !== 'versions' && ( +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ )} + {resultsType === 'versions' && ( + }> + + + )} +
+
); }; diff --git a/src/components/SearchResults.tsx b/src/components/SearchResults.tsx new file mode 100644 index 0000000..0fa43b8 --- /dev/null +++ b/src/components/SearchResults.tsx @@ -0,0 +1,173 @@ +import { useMemo } from 'react'; +import { Artist, SearchResuts, SearchResultsType, SongVersions, Song } from '../types'; +import { sortByKey } from '@/lib/utils'; +import Column from './Column'; +import Row from './Row'; + +type SearchResutsProps = { + data: SearchResuts | null; + getSources: (slim_artist: Artist | undefined, songUuid: string | undefined) => void; + sortBy: string; + resultsType: SearchResultsType; + versionsData: SongVersions | null; +}; + +// "1984-12-01T00:00:00Z" -> "1984/12/01" +function dateStringToPathSegment(inputDateStr) { + const originalDate = new Date(inputDateStr); + const yyyy = originalDate.getUTCFullYear(); + const mm = String(originalDate.getUTCMonth() + 1).padStart(2, '0'); + const dd = String(originalDate.getUTCDate()).padStart(2, '0'); + + return `${yyyy}/${mm}/${dd}`; +} + +export default function SearchResults({ + data, + getSources, + sortBy, + resultsType, + versionsData, +}: SearchResutsProps) { + const sortedData = useMemo(() => { + if (!data) { + return null; + } + + const sortedSongs = sortByKey('shows_played_at', data.Songs); + // sortedSongs = sortByKey('sortName', data.Songs); + + return { + ...data, + Artists: sortByKey('sort_name', data.Artists) as Artist[], + Songs: sortedSongs as Song[], + }; + }, [data]); + + const sortedVersions = useMemo(() => { + if (!versionsData) { + return null; + } + + return { + ...versionsData, + shows: [...versionsData.shows].sort((a, b) => { + if (a.date === undefined || b.date === undefined) { + return 0; // TODO: Handle the case where either value is undefined + } + + if (new Date(a.date).getTime() > new Date(b.date).getTime()) { + return sortBy === 'DATE_ASC' ? 1 : -1; + } + + if (new Date(a.date).getTime() < new Date(b.date).getTime()) { + return sortBy === 'DATE_ASC' ? -1 : 1; + } + + return 0; + }), + }; + }, [sortBy, versionsData]); + + if (sortedData === null) { + return ( + // eslint-disable-next-line react/jsx-no-comment-textnodes +
// TODO: There is no search query, so put the featured artists list here
+ ); + } + + if (resultsType === 'artists' && sortedData?.Artists.length) { + return ( + + {sortedData?.Artists.map(({ name, uuid, slug }) => ( + +
+
{name}
+
+
+
ARTIST
+
+
+ ))} +
+ ); + } + + if (resultsType === 'songs' && sortedData?.Songs.length) { + return ( + + {sortByKey('shows_played_at', sortedData?.Songs).map( + ({ name, uuid, slim_artist }: Song) => ( + + ) + )} + + ); + } + + if (resultsType === 'all' && (sortedData?.Artists.length || sortedData?.Songs.length)) { + return ( + + {sortedData?.Artists.map(({ name, uuid, slug }) => ( + +
+
{name}
+
+
+
ARTIST
+
+
+ ))} + {sortByKey('shows_played_at', sortedData?.Songs).map( + ({ name, uuid, slim_artist }: Song) => ( + + ) + )} +
+ ); + } + + if (resultsType === 'versions' && sortedVersions) { + return sortedVersions?.shows?.map(({ date, display_date, venue, uuid }) => { + // https://relisten.net/grateful-dead/1994/12/15/me-and-my-uncle?source=346445 + return ( + +
+
{sortedVersions?.name}
+
+
+ {venue?.sortName} · {venue?.location} +
+
{display_date}
+
+
+ {/* // TODO: where is the duration? */} +
12:00
+
+ ); + }); + } + + return
No results
; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 4e0915f..4e9e971 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -74,3 +74,22 @@ export const groupBy = function (xs, key) { return rv; }, {}); }; + +// Sort data[] by data.key +export function sortByKey(key: string, data: object[]) { + return [...data].sort((a, b) => { + if (typeof a[key] === 'string' && typeof b[key] === 'string') { + return b[key].localeCompare(a[key]); + } + + if (a[key] < b[key]) { + return 1; + } + + if (a[key] > b[key]) { + return -1; + } + + return 0; + }); +} diff --git a/src/types.ts b/src/types.ts index 1bb00b6..7898c00 100644 --- a/src/types.ts +++ b/src/types.ts @@ -274,6 +274,7 @@ export type Source = { num_ratings?: number | null; avg_rating_weighted?: number; duration?: number; + slim_artist?: Artist; upstream_identifier?: string; uuid?: string; created_at?: string; @@ -369,3 +370,14 @@ export type SearchResuts = { Tours: Tour[]; Venues: Venue[]; }; + +// Which type of search results are being shown on the search page +export type SearchResultsType = 'all' | 'songs' | 'artists' | 'versions'; + +export type SongVersions = { + name: string; + shows: Show[]; + slug: string; + artistName: string; + artistSlug: string; +}; From 688c7c36b71fb805f30e0647d44fea2f4c1b8bbe Mon Sep 17 00:00:00 2001 From: Jared Salzano Date: Fri, 5 Jan 2024 18:36:53 -0500 Subject: [PATCH 03/10] Remove console.logs --- src/components/Search.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 6eaa0f9..fbdf38d 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -105,12 +105,6 @@ const Search = () => { [] ); - useEffect(() => { - console.log('data: ', data); - console.log('sortBy: ', sortBy); - console.log('versionsData: ', versionsData); - }, [sortBy, data, versionsData]); - return (
{resultsType === 'versions' ? ( From 513c2e9828146a2b7b918a79f2bdeb5dd00f4ca9 Mon Sep 17 00:00:00 2001 From: Jared Salzano Date: Wed, 15 May 2024 15:52:45 -0400 Subject: [PATCH 04/10] Convert search to server components; add w-full class to secondary layout --- src/app/(main)/(secondary)/layout.tsx | 2 +- src/app/(main)/(secondary)/search/page.tsx | 86 +++++++- src/components/Search.tsx | 184 ------------------ src/components/search/SearchBar.tsx | 71 +++++++ src/components/search/SearchFilterPill.tsx | 34 ++++ src/components/{ => search}/SearchResults.tsx | 68 ++++--- src/components/search/SearchSortByMenu.tsx | 33 ++++ src/lib/useDebouncedEffect.ts | 19 -- src/styles/globals.css | 15 ++ src/types.ts | 13 +- 10 files changed, 293 insertions(+), 232 deletions(-) delete mode 100644 src/components/Search.tsx create mode 100644 src/components/search/SearchBar.tsx create mode 100644 src/components/search/SearchFilterPill.tsx rename src/components/{ => search}/SearchResults.tsx (76%) create mode 100644 src/components/search/SearchSortByMenu.tsx delete mode 100644 src/lib/useDebouncedEffect.ts diff --git a/src/app/(main)/(secondary)/layout.tsx b/src/app/(main)/(secondary)/layout.tsx index fb4ea61..4be89be 100644 --- a/src/app/(main)/(secondary)/layout.tsx +++ b/src/app/(main)/(secondary)/layout.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren } from 'react'; export default function Layout({ children }: PropsWithChildren) { - return
{children}
; + return
{children}
; } diff --git a/src/app/(main)/(secondary)/search/page.tsx b/src/app/(main)/(secondary)/search/page.tsx index 912c093..bfaa1b3 100644 --- a/src/app/(main)/(secondary)/search/page.tsx +++ b/src/app/(main)/(secondary)/search/page.tsx @@ -1,9 +1,89 @@ -import Search from '@/components/Search'; +import { SearchParams, SearchResults, SearchResultsType, SongVersions } from '@/types'; +import { API_DOMAIN } from '@/lib/constants'; +import { SimplePopover } from '@/components/Popover'; +import SearchBar from '@/components/search/SearchBar'; +import SearchFilterPill from '@/components/search/SearchFilterPill'; +import SearchResultsCpt from '@/components/search/SearchResults'; +import SearchSortByMenu from '@/components/search/SearchSortByMenu'; + +export default async function Page({ searchParams }: { searchParams: SearchParams }) { + let data: SearchResults | null = null; + let resultsType: SearchResultsType = searchParams.resultsType || 'all'; + let versionsData: SongVersions | null = null; + + if (searchParams.q) { + const response = await fetch(`${API_DOMAIN}/api/v2/search?q=${searchParams.q}`, { + cache: 'no-cache', // seconds + }).then((res) => res.json()); + + // API sometimes returns null instead of [] + data = { ...response, Songs: !response.Songs ? [] : response.Songs }; + } + + if ( + searchParams.artistUuid && + searchParams.songUuid && + searchParams.artistName && + searchParams.artistSlug + ) { + const response = await fetch( + `${API_DOMAIN}/api/v3/artists/${searchParams.artistUuid}/songs/${searchParams.songUuid}`, + { + cache: 'no-cache', // seconds + } + ).then((res) => res.json()); + + resultsType = 'versions'; + versionsData = { + ...response, + artistName: searchParams.artistName, + artistSlug: searchParams.artistSlug, + }; + } -export default async function Page() { return (
- + +
+ {data !== null && resultsType === 'versions' && ( +
+ Showing all versions of “{versionsData?.name}” by {versionsData?.artistName} +
+ )} + {data !== null && resultsType !== 'versions' && ( +
    +
  • + + All {data && `(${data?.Artists?.length + data?.Songs?.length})`} + +
  • +
  • + + Artists {data && `(${data?.Artists?.length})`} + +
  • +
  • + + Songs {data && `(${data?.Songs?.length})`} + +
  • +
+ )} + {resultsType === 'versions' && ( + } + position="bottom-end" + > + + + )} +
+
); } diff --git a/src/components/Search.tsx b/src/components/Search.tsx deleted file mode 100644 index fbdf38d..0000000 --- a/src/components/Search.tsx +++ /dev/null @@ -1,184 +0,0 @@ -'use client'; - -import { useCallback, useEffect, useState } from 'react'; -import useDebouncedEffect from '../lib/useDebouncedEffect'; -import { API_DOMAIN } from '../lib/constants'; -import SearchResults from './SearchResults'; -import { Artist, SearchResuts, SearchResultsType, SongVersions } from '../types'; -import Column from './Column'; -import { SimplePopover } from './Popover'; -import Row from './Row'; - -async function fetchSearchResults(query: string) { - const data: SearchResuts = await fetch(`${API_DOMAIN}/api/v2/search?q=${query}`, { - cache: 'no-cache', // seconds - }).then((res) => res.json()); - - return data; -} - -const SortByMenu = ({ - resultsType, - setSortBy, -}: { - resultsType: SearchResultsType; - setSortBy: (string) => void; -}) => { - if (resultsType === 'all') { - return null; - } - - if (resultsType === 'artists') { - return null; - } - - if (resultsType === 'songs') { - return null; - } - - if (resultsType === 'versions') { - return ( - - - - - ); - } -}; - -const Search = () => { - const [data, setData] = useState(null); - const [versionsData, setVersionsData] = useState(null); - const [resultsType, setResultsType] = useState('all'); - const [searchValue, setSearchValue] = useState(''); - const [sortBy, setSortBy] = useState('DATE_ASC'); - - useDebouncedEffect( - () => { - // TODO: sanitize, trim ... - if (searchValue) { - fetchSearchResults(searchValue).then((r) => { - // API sometimes returns null instead of [] - const songs = !r.Songs ? [] : r.Songs; - setData({ ...r, Songs: songs }); - }); - } else { - setData(null); - } - }, - 200, - [searchValue] - ); - - const clearSearch = useCallback(() => { - setSearchValue(''); - setData(null); - setResultsType('all'); - setVersionsData(null); - setSortBy(''); - }, []); - - const getSources = useCallback( - (slim_artist: Artist | undefined, songUuid: string | undefined) => { - if (!songUuid || !slim_artist || !slim_artist.slug) { - // TODO: handle error - return; - } - - fetch(`${API_DOMAIN}/api/v3/artists/${slim_artist.uuid}/songs/${songUuid}`, { - cache: 'no-cache', // seconds - }).then((res) => { - res.json().then((json) => { - setResultsType('versions'); - setVersionsData({ - ...json, - artistName: slim_artist.name, - artistSlug: slim_artist.slug, - }); - }); - }); - }, - [] - ); - - return ( -
- {resultsType === 'versions' ? ( - - ) : ( -
- - setSearchValue(e.target.value)} - /> - -
- )} -
- {data !== null && resultsType === 'versions' && ( -
- Showing all versions of “{versionsData?.name}” by {versionsData?.artistName} -
- )} - {data !== null && resultsType !== 'versions' && ( -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
- )} - {resultsType === 'versions' && ( - }> - - - )} -
- -
- ); -}; - -export default Search; diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx new file mode 100644 index 0000000..3f66f2c --- /dev/null +++ b/src/components/search/SearchBar.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { SearchResultsType } from '@/types'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { FormEvent, useState, useTransition } from 'react'; + +export default function SearchBar({ resultsType }: { resultsType: SearchResultsType }) { + const pathname = usePathname(); + const router = useRouter(); + const searchParams = useSearchParams(); + const writableParams = new URLSearchParams(searchParams); + const [value, setValue] = useState(searchParams?.get('q')?.toString() || ''); + const [isPending, startTransition] = useTransition(); + + function handleSubmit(e?: FormEvent) { + if (e) { + e.preventDefault(); + } + + if (value) { + writableParams.set('q', value); + } else { + writableParams.delete('q'); + } + + startTransition(() => router.replace(`${pathname}?${writableParams.toString()}`)); + } + + function clearSearch() { + writableParams.delete('artistName'); + writableParams.delete('artistSlug'); + writableParams.delete('artistUuid'); + writableParams.delete('q'); + writableParams.delete('resultsType'); + writableParams.delete('songUuid'); + writableParams.delete('sortBy'); + startTransition(() => { + setValue(''); + router.replace(`${pathname}?${writableParams.toString()}`); + }); + } + + return ( +
+ {resultsType === 'versions' ? ( + + ) : ( +
+ + setValue(e.target.value)} + /> + +
+ )} + + ); +} diff --git a/src/components/search/SearchFilterPill.tsx b/src/components/search/SearchFilterPill.tsx new file mode 100644 index 0000000..be9d6f2 --- /dev/null +++ b/src/components/search/SearchFilterPill.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { SearchResultsType } from '@/types'; +import React from 'react'; + +export default function SearchFilterPill({ + buttonType, + children, + resultsType, +}: { + buttonType: SearchResultsType; + children: React.ReactNode; + resultsType: SearchResultsType; +}) { + const pathname = usePathname(); + const router = useRouter(); + const searchParams = useSearchParams(); + const writableParams = new URLSearchParams(searchParams); + + function handleClick() { + writableParams.set('resultsType', buttonType); + router.replace(`${pathname}?${writableParams.toString()}`); + } + + return ( + + ); +} diff --git a/src/components/SearchResults.tsx b/src/components/search/SearchResults.tsx similarity index 76% rename from src/components/SearchResults.tsx rename to src/components/search/SearchResults.tsx index 0fa43b8..dfc9365 100644 --- a/src/components/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -1,16 +1,18 @@ +'use client'; + import { useMemo } from 'react'; -import { Artist, SearchResuts, SearchResultsType, SongVersions, Song } from '../types'; +import { + Artist, + SearchParams, + SearchResults as TSearchResults, + SearchResultsType, + SongVersions, + Song, +} from '../../types'; +import { usePathname, useRouter } from 'next/navigation'; import { sortByKey } from '@/lib/utils'; -import Column from './Column'; -import Row from './Row'; - -type SearchResutsProps = { - data: SearchResuts | null; - getSources: (slim_artist: Artist | undefined, songUuid: string | undefined) => void; - sortBy: string; - resultsType: SearchResultsType; - versionsData: SongVersions | null; -}; +import Column from '../Column'; +import Row from '../Row'; // "1984-12-01T00:00:00Z" -> "1984/12/01" function dateStringToPathSegment(inputDateStr) { @@ -24,23 +26,28 @@ function dateStringToPathSegment(inputDateStr) { export default function SearchResults({ data, - getSources, - sortBy, resultsType, + searchParams, versionsData, -}: SearchResutsProps) { +}: { + data: TSearchResults | null; + resultsType: SearchResultsType; + searchParams: SearchParams; + versionsData: SongVersions | null; +}) { + const pathname = usePathname(); + const router = useRouter(); + const writableParams = new URLSearchParams(searchParams); + const sortedData = useMemo(() => { if (!data) { return null; } - const sortedSongs = sortByKey('shows_played_at', data.Songs); - // sortedSongs = sortByKey('sortName', data.Songs); - return { ...data, Artists: sortByKey('sort_name', data.Artists) as Artist[], - Songs: sortedSongs as Song[], + Songs: sortByKey('shows_played_at', data.Songs) as Song[], // sortByKey('sortName', data.Songs) }; }, [data]); @@ -53,21 +60,34 @@ export default function SearchResults({ ...versionsData, shows: [...versionsData.shows].sort((a, b) => { if (a.date === undefined || b.date === undefined) { - return 0; // TODO: Handle the case where either value is undefined + return 0; } if (new Date(a.date).getTime() > new Date(b.date).getTime()) { - return sortBy === 'DATE_ASC' ? 1 : -1; + return searchParams.sortBy === 'DATE_ASC' ? 1 : -1; } if (new Date(a.date).getTime() < new Date(b.date).getTime()) { - return sortBy === 'DATE_ASC' ? -1 : 1; + return searchParams.sortBy === 'DATE_ASC' ? -1 : 1; } return 0; }), }; - }, [sortBy, versionsData]); + }, [searchParams.sortBy, versionsData]); + + function setSources(slim_artist: Artist | undefined, songUuid: string | undefined) { + if (!slim_artist || !slim_artist.name || !slim_artist.slug || !slim_artist.uuid || !songUuid) { + // TODO: handle error + return; + } + + writableParams.set('artistName', slim_artist.name); + writableParams.set('artistSlug', slim_artist.slug); + writableParams.set('artistUuid', slim_artist.uuid); + writableParams.set('songUuid', songUuid); + router.replace(`${pathname}?${writableParams.toString()}`); + } if (sortedData === null) { return ( @@ -98,7 +118,7 @@ export default function SearchResults({ {sortByKey('shows_played_at', sortedData?.Songs).map( ({ name, uuid, slim_artist }: Song) => ( - + + + ); +} diff --git a/src/lib/useDebouncedEffect.ts b/src/lib/useDebouncedEffect.ts deleted file mode 100644 index 54a150c..0000000 --- a/src/lib/useDebouncedEffect.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect, useRef } from 'react'; - -export default function useDebouncedEffect(effect, delay, dependencies) { - const effectRef = useRef(effect); - - useEffect(() => { - effectRef.current = effect; - }, [effect]); - - useEffect(() => { - const handler = setTimeout(() => { - effectRef.current(); - }, delay); - - return () => { - clearTimeout(handler); - }; - }, [...dependencies, delay]); -} diff --git a/src/styles/globals.css b/src/styles/globals.css index 39ac49a..b107f4d 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -34,6 +34,10 @@ outline: none; } + .search-bar > .fa-spinner { + animation: spin .7s linear infinite; + } + .search-filters > li { @apply inline-block; } @@ -47,5 +51,16 @@ } } +@layer utilities { + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +} + body { margin: 0; font-family: Roboto, Helvetica, Helvetica Neue, sans-serif; -webkit-font-smoothing: antialiased; color: #333; } a { text-decoration: none; color: #333; } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 7898c00..34f719c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -362,7 +362,18 @@ export type ActiveTrack = { webAudioLoadingState?: string; }; -export type SearchResuts = { +// query-string parameters for search page +export type SearchParams = { + artistName: string; + artistSlug: string; + artistUuid: string; + q: string; + resultsType: SearchResultsType; + songUuid: string; + sortBy?: 'DATE_ASC' | 'DATE_DESC'; +}; + +export type SearchResults = { Artists: Artist[]; Shows: Show[]; Songs: Song[]; From 104a564e7bf29647ee16a4f97c943be142fca85d Mon Sep 17 00:00:00 2001 From: Jared Salzano Date: Wed, 15 May 2024 16:45:13 -0400 Subject: [PATCH 05/10] search: Convert buttons to links, and convert search components to server components --- src/app/(main)/(secondary)/search/page.tsx | 20 ++++++++-- src/components/search/SearchFilterPill.tsx | 25 +++++------- src/components/search/SearchResults.tsx | 44 +++++++++------------- src/components/search/SearchSortByMenu.tsx | 31 +++++++-------- src/styles/globals.css | 2 +- 5 files changed, 60 insertions(+), 62 deletions(-) diff --git a/src/app/(main)/(secondary)/search/page.tsx b/src/app/(main)/(secondary)/search/page.tsx index bfaa1b3..0d7c510 100644 --- a/src/app/(main)/(secondary)/search/page.tsx +++ b/src/app/(main)/(secondary)/search/page.tsx @@ -53,17 +53,29 @@ export default async function Page({ searchParams }: { searchParams: SearchParam {data !== null && resultsType !== 'versions' && (
  • - + All {data && `(${data?.Artists?.length + data?.Songs?.length})`}
  • - + Artists {data && `(${data?.Artists?.length})`}
  • - + Songs {data && `(${data?.Songs?.length})`}
  • @@ -71,7 +83,7 @@ export default async function Page({ searchParams }: { searchParams: SearchParam )} {resultsType === 'versions' && ( } + content={} position="bottom-end" > diff --git a/src/components/search/SearchFilterPill.tsx b/src/components/search/SearchFilterPill.tsx index be9d6f2..dbf514a 100644 --- a/src/components/search/SearchFilterPill.tsx +++ b/src/components/search/SearchFilterPill.tsx @@ -1,34 +1,29 @@ -'use client'; - -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { SearchResultsType } from '@/types'; +import Link from 'next/link'; +import { SearchParams, SearchResultsType } from '@/types'; import React from 'react'; export default function SearchFilterPill({ buttonType, children, resultsType, + searchParams, }: { buttonType: SearchResultsType; children: React.ReactNode; resultsType: SearchResultsType; + searchParams: SearchParams; }) { - const pathname = usePathname(); - const router = useRouter(); - const searchParams = useSearchParams(); const writableParams = new URLSearchParams(searchParams); - function handleClick() { - writableParams.set('resultsType', buttonType); - router.replace(`${pathname}?${writableParams.toString()}`); - } + writableParams.set('resultsType', buttonType); return ( - + ); } diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index dfc9365..a28c1b1 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -1,6 +1,3 @@ -'use client'; - -import { useMemo } from 'react'; import { Artist, SearchParams, @@ -9,7 +6,7 @@ import { SongVersions, Song, } from '../../types'; -import { usePathname, useRouter } from 'next/navigation'; +import Link from 'next/link'; import { sortByKey } from '@/lib/utils'; import Column from '../Column'; import Row from '../Row'; @@ -35,28 +32,20 @@ export default function SearchResults({ searchParams: SearchParams; versionsData: SongVersions | null; }) { - const pathname = usePathname(); - const router = useRouter(); const writableParams = new URLSearchParams(searchParams); + let sortedData: TSearchResults | null = null; + let sortedVersions: SongVersions | null = null; - const sortedData = useMemo(() => { - if (!data) { - return null; - } - - return { + if (data) { + sortedData = { ...data, Artists: sortByKey('sort_name', data.Artists) as Artist[], Songs: sortByKey('shows_played_at', data.Songs) as Song[], // sortByKey('sortName', data.Songs) }; - }, [data]); - - const sortedVersions = useMemo(() => { - if (!versionsData) { - return null; - } + } - return { + if (versionsData) { + sortedVersions = { ...versionsData, shows: [...versionsData.shows].sort((a, b) => { if (a.date === undefined || b.date === undefined) { @@ -74,19 +63,20 @@ export default function SearchResults({ return 0; }), }; - }, [searchParams.sortBy, versionsData]); + } - function setSources(slim_artist: Artist | undefined, songUuid: string | undefined) { + function getHref(slim_artist: Artist | undefined, songUuid: string | undefined) { if (!slim_artist || !slim_artist.name || !slim_artist.slug || !slim_artist.uuid || !songUuid) { // TODO: handle error - return; + return 'search'; } writableParams.set('artistName', slim_artist.name); writableParams.set('artistSlug', slim_artist.slug); writableParams.set('artistUuid', slim_artist.uuid); writableParams.set('songUuid', songUuid); - router.replace(`${pathname}?${writableParams.toString()}`); + + return `search?${writableParams.toString()}`; } if (sortedData === null) { @@ -118,7 +108,7 @@ export default function SearchResults({ {sortByKey('shows_played_at', sortedData?.Songs).map( ({ name, uuid, slim_artist }: Song) => ( - + ) )} @@ -148,7 +138,7 @@ export default function SearchResults({ ))} {sortByKey('shows_played_at', sortedData?.Songs).map( ({ name, uuid, slim_artist }: Song) => ( - + ) )} diff --git a/src/components/search/SearchSortByMenu.tsx b/src/components/search/SearchSortByMenu.tsx index 5f87a95..567b0b7 100644 --- a/src/components/search/SearchSortByMenu.tsx +++ b/src/components/search/SearchSortByMenu.tsx @@ -1,19 +1,20 @@ -'use client'; - -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { SearchResultsType } from '@/types'; +import { SearchParams, SearchResultsType } from '@/types'; +import Link from 'next/link'; import Column from '../Column'; import Row from '../Row'; -export default function SortByMenu({ resultsType }: { resultsType: SearchResultsType }) { - const pathname = usePathname(); - const router = useRouter(); - const searchParams = useSearchParams(); +export default function SortByMenu({ + resultsType, + searchParams, +}: { + resultsType: SearchResultsType; + searchParams: SearchParams; +}) { const writableParams = new URLSearchParams(searchParams); - function handleClick(value) { - writableParams.set('sortBy', value); - router.replace(`${pathname}?${writableParams.toString()}`); + function getHref(sortBy) { + writableParams.set('sortBy', sortBy); + return `search?${writableParams.toString()}`; } if (resultsType !== 'versions') { @@ -22,12 +23,12 @@ export default function SortByMenu({ resultsType }: { resultsType: SearchResults return ( - - + ); } diff --git a/src/styles/globals.css b/src/styles/globals.css index b107f4d..225513e 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -46,7 +46,7 @@ @apply rounded-full px-2 py-1; } - .search-filters > li > button.search-filters-button--active { + .search-filters > li > .search-filters-item--active { @apply bg-relisten-100 text-white; } } From 3e6e21c1eb6a17a19e841c129d4be59f3ca00a4f Mon Sep 17 00:00:00 2001 From: Jared Salzano Date: Fri, 17 May 2024 14:41:47 -0400 Subject: [PATCH 06/10] search: Remove artist name and artist slug from search params, making an additional API call on server to get them from the artist uuid --- src/app/(main)/(secondary)/search/page.tsx | 32 ++++++++++++---------- src/components/search/SearchBar.tsx | 2 -- src/components/search/SearchResults.tsx | 4 +-- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/app/(main)/(secondary)/search/page.tsx b/src/app/(main)/(secondary)/search/page.tsx index 0d7c510..fd346d0 100644 --- a/src/app/(main)/(secondary)/search/page.tsx +++ b/src/app/(main)/(secondary)/search/page.tsx @@ -20,24 +20,26 @@ export default async function Page({ searchParams }: { searchParams: SearchParam data = { ...response, Songs: !response.Songs ? [] : response.Songs }; } - if ( - searchParams.artistUuid && - searchParams.songUuid && - searchParams.artistName && - searchParams.artistSlug - ) { - const response = await fetch( - `${API_DOMAIN}/api/v3/artists/${searchParams.artistUuid}/songs/${searchParams.songUuid}`, - { - cache: 'no-cache', // seconds - } - ).then((res) => res.json()); + if (searchParams.artistUuid && searchParams.songUuid) { + const [artistResponse, versionsRespose] = await Promise.all([ + fetch(`${API_DOMAIN}/api/v3/artists/${searchParams.artistUuid}`, { cache: 'no-cache' }), + fetch( + `${API_DOMAIN}/api/v3/artists/${searchParams.artistUuid}/songs/${searchParams.songUuid}`, + { cache: 'no-cache' } + ), + ]); + + const [artistJson, versionsJson] = await Promise.all([ + artistResponse.json(), + versionsRespose.json(), + ]); resultsType = 'versions'; + versionsData = { - ...response, - artistName: searchParams.artistName, - artistSlug: searchParams.artistSlug, + ...versionsJson, + artistName: artistJson.name, + artistSlug: artistJson.slug, }; } diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 3f66f2c..4198897 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -27,8 +27,6 @@ export default function SearchBar({ resultsType }: { resultsType: SearchResultsT } function clearSearch() { - writableParams.delete('artistName'); - writableParams.delete('artistSlug'); writableParams.delete('artistUuid'); writableParams.delete('q'); writableParams.delete('resultsType'); diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index a28c1b1..40a0ced 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -66,13 +66,11 @@ export default function SearchResults({ } function getHref(slim_artist: Artist | undefined, songUuid: string | undefined) { - if (!slim_artist || !slim_artist.name || !slim_artist.slug || !slim_artist.uuid || !songUuid) { + if (!slim_artist || !slim_artist.uuid || !songUuid) { // TODO: handle error return 'search'; } - writableParams.set('artistName', slim_artist.name); - writableParams.set('artistSlug', slim_artist.slug); writableParams.set('artistUuid', slim_artist.uuid); writableParams.set('songUuid', songUuid); From 4c980fca8629ef01485d81074bf50606ed48e748 Mon Sep 17 00:00:00 2001 From: Jared Salzano Date: Mon, 20 May 2024 15:29:39 -0400 Subject: [PATCH 07/10] Search: Remove artistUuid from query params, get it on backend; use date-fns instead of custom function --- src/app/(main)/(secondary)/search/page.tsx | 26 +++++++++------------- src/components/search/SearchBar.tsx | 1 - src/components/search/SearchResults.tsx | 16 ++----------- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/app/(main)/(secondary)/search/page.tsx b/src/app/(main)/(secondary)/search/page.tsx index fd346d0..eb9bd47 100644 --- a/src/app/(main)/(secondary)/search/page.tsx +++ b/src/app/(main)/(secondary)/search/page.tsx @@ -20,26 +20,22 @@ export default async function Page({ searchParams }: { searchParams: SearchParam data = { ...response, Songs: !response.Songs ? [] : response.Songs }; } - if (searchParams.artistUuid && searchParams.songUuid) { - const [artistResponse, versionsRespose] = await Promise.all([ - fetch(`${API_DOMAIN}/api/v3/artists/${searchParams.artistUuid}`, { cache: 'no-cache' }), - fetch( - `${API_DOMAIN}/api/v3/artists/${searchParams.artistUuid}/songs/${searchParams.songUuid}`, - { cache: 'no-cache' } - ), - ]); + if (data?.Songs.length && searchParams.songUuid) { + const song = data.Songs.find(({ uuid }) => uuid === searchParams.songUuid); - const [artistJson, versionsJson] = await Promise.all([ - artistResponse.json(), - versionsRespose.json(), - ]); + let versions = await fetch( + `${API_DOMAIN}/api/v3/artists/${song?.slim_artist?.uuid}/songs/${searchParams.songUuid}`, + { cache: 'no-cache' } + ); + + versions = await versions.json(); resultsType = 'versions'; versionsData = { - ...versionsJson, - artistName: artistJson.name, - artistSlug: artistJson.slug, + ...versions, + artistName: song?.slim_artist?.name || '', + artistSlug: song?.slim_artist?.slug || '', }; } diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 4198897..3c44593 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -27,7 +27,6 @@ export default function SearchBar({ resultsType }: { resultsType: SearchResultsT } function clearSearch() { - writableParams.delete('artistUuid'); writableParams.delete('q'); writableParams.delete('resultsType'); writableParams.delete('songUuid'); diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index 40a0ced..affd84e 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -10,16 +10,7 @@ import Link from 'next/link'; import { sortByKey } from '@/lib/utils'; import Column from '../Column'; import Row from '../Row'; - -// "1984-12-01T00:00:00Z" -> "1984/12/01" -function dateStringToPathSegment(inputDateStr) { - const originalDate = new Date(inputDateStr); - const yyyy = originalDate.getUTCFullYear(); - const mm = String(originalDate.getUTCMonth() + 1).padStart(2, '0'); - const dd = String(originalDate.getUTCDate()).padStart(2, '0'); - - return `${yyyy}/${mm}/${dd}`; -} +import { formatInTimeZone } from 'date-fns-tz'; export default function SearchResults({ data, @@ -71,7 +62,6 @@ export default function SearchResults({ return 'search'; } - writableParams.set('artistUuid', slim_artist.uuid); writableParams.set('songUuid', songUuid); return `search?${writableParams.toString()}`; @@ -157,9 +147,7 @@ export default function SearchResults({ return (
    {sortedVersions?.name}
    From 1971fb71ea70ac8a9c0c37a16b111075f32287f6 Mon Sep 17 00:00:00 2001 From: Jared Salzano Date: Mon, 20 May 2024 15:50:56 -0400 Subject: [PATCH 08/10] search: If there are query params, back button should go to /search, otherwise leave the search page --- src/components/search/SearchBar.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 3c44593..a7d6774 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -17,13 +17,24 @@ export default function SearchBar({ resultsType }: { resultsType: SearchResultsT e.preventDefault(); } + /** If there are query params, back button should go to /search, otherwise leave the search page */ + const shouldPush = writableParams.toString() === ''; + if (value) { writableParams.set('q', value); } else { writableParams.delete('q'); } - startTransition(() => router.replace(`${pathname}?${writableParams.toString()}`)); + const path = `${pathname}?${writableParams.toString()}`; + + startTransition(() => { + if (shouldPush) { + router.push(path); + } else { + router.replace(path); + } + }); } function clearSearch() { From 68f769ba9e056e4c7402732eee72add5100c9bad Mon Sep 17 00:00:00 2001 From: Jared Salzano Date: Mon, 20 May 2024 15:57:36 -0400 Subject: [PATCH 09/10] Search: remove cache option in fetch; avoid instance of var reassignment --- src/app/(main)/(secondary)/search/page.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/app/(main)/(secondary)/search/page.tsx b/src/app/(main)/(secondary)/search/page.tsx index eb9bd47..842126a 100644 --- a/src/app/(main)/(secondary)/search/page.tsx +++ b/src/app/(main)/(secondary)/search/page.tsx @@ -12,9 +12,9 @@ export default async function Page({ searchParams }: { searchParams: SearchParam let versionsData: SongVersions | null = null; if (searchParams.q) { - const response = await fetch(`${API_DOMAIN}/api/v2/search?q=${searchParams.q}`, { - cache: 'no-cache', // seconds - }).then((res) => res.json()); + const response = await fetch(`${API_DOMAIN}/api/v2/search?q=${searchParams.q}`).then((res) => + res.json() + ); // API sometimes returns null instead of [] data = { ...response, Songs: !response.Songs ? [] : response.Songs }; @@ -23,12 +23,9 @@ export default async function Page({ searchParams }: { searchParams: SearchParam if (data?.Songs.length && searchParams.songUuid) { const song = data.Songs.find(({ uuid }) => uuid === searchParams.songUuid); - let versions = await fetch( - `${API_DOMAIN}/api/v3/artists/${song?.slim_artist?.uuid}/songs/${searchParams.songUuid}`, - { cache: 'no-cache' } - ); - - versions = await versions.json(); + const versions = await fetch( + `${API_DOMAIN}/api/v3/artists/${song?.slim_artist?.uuid}/songs/${searchParams.songUuid}` + ).then((res) => res.json()); resultsType = 'versions'; From 56e6b645fb2bccc6500c991a8369740dabbd0acc Mon Sep 17 00:00:00 2001 From: Jared Salzano Date: Mon, 20 May 2024 16:02:31 -0400 Subject: [PATCH 10/10] Update package.json --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index fc4f915..8d5b631 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@tanstack/react-query-next-experimental": "^5.35.1", "@types/react-redux": "^7.1.33", "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", "framer-motion": "^11.1.9", "isomorphic-fetch": "3.0.0", "ky": "^1.2.4",