Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/app/(main)/(secondary)/search/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Search from '@/components/Search';

export default async function Page() {
return (
<div className="mx-auto w-full max-w-screen-md flex-1">
<Search />
</div>
);
}

export const metadata = {
title: 'Search',
};
16 changes: 12 additions & 4 deletions src/app/(main)/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | undefined>);
return memo;
},
{} as Record<string, string | undefined>
);

return (
<div className="relative grid h-[50px] max-h-[50px] min-h-[50px] grid-cols-3 justify-between border-b-[1px] border-b-[#aeaeae] bg-white text-[#333333] max-lg:flex">
Expand All @@ -29,6 +32,11 @@ export default async function NavBar() {
</Flex>
</SimplePopover>
<div className="nav hidden h-full flex-[2] cursor-pointer items-center justify-end text-center font-medium lg:flex">
<div className="h-full px-1">
<Link href="/search" legacyBehavior prefetch={false}>
<a className="nav-btn">SEARCH</a>
</Link>
</div>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running out of room on the nav. This goes behind the player

<div className="h-full px-1">
<Link href="/today" legacyBehavior prefetch={false}>
<a className="nav-btn">TIH</a>
Expand Down
102 changes: 102 additions & 0 deletions src/components/Search.tsx
Original file line number Diff line number Diff line change
@@ -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<SearchResuts | null>(null);
const [activeFilter, setActiveFilter] = useState<string>('artists');
const [searchValue, setSearchValue] = useState<string>('');

useDebouncedEffect(
() => {
// TODO: sanitize, trim ...
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prepare the string so it's always a valid param

if (searchValue) {
fetchResults(searchValue).then(setData);
} else {
setData(null);
}
},
200,
[searchValue]
);

return (
<div className="w-screen max-w-screen-md">
<div className="search-bar mb-2 flex items-center p-2">
<i className="fa fa-search px-2" />
<input
className="grow"
type="text"
placeholder="Search..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
<button onClick={() => setSearchValue('')} aria-label="clear search" className="flex">
<i className="fa fa-times px-2" />
</button>
</div>
<ul className="search-filters pb-4">
<li>
<button
onClick={() => setActiveFilter('artists')}
className={activeFilter === 'artists' ? 'search-filters-button--active' : ''}
>
Artists
</button>
</li>
<li>
<button
onClick={() => setActiveFilter('songs')}
className={activeFilter === 'songs' ? 'search-filters-button--active' : ''}
>
Songs
</button>
</li>
</ul>
<Column loading={!data}>
{activeFilter === 'artists' &&
data?.Artists?.map(({ name, uuid, slug, show_count, source_count }) => {
return (
<Row key={uuid} href={`/${slug}`}>
<div>
<div>{name}</div>
</div>
<div className="min-w-[20%] text-right text-xs text-[#979797]">
<div>ARTIST</div>
</div>
</Row>
);
})}
{activeFilter === 'songs' &&
data?.Songs?.map(({ name, uuid, slim_artist }) => (
<Row
key={uuid}
// TODO: more data needed for song link
href={slim_artist ? `/${slim_artist.slug}` : undefined}
>
<div>
<div>{name}</div>
<div className="text-xxs text-gray-400">{slim_artist?.name}</div>
</div>
<div className="min-w-[20%] text-right text-xxs text-gray-400">SONG</div>
</Row>
))}
</Column>
</div>
);
};

export default Search;
19 changes: 19 additions & 0 deletions src/lib/useDebouncedEffect.ts
Original file line number Diff line number Diff line change
@@ -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]);
}
13 changes: 13 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
24 changes: 24 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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[];
};