Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
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
2 changes: 1 addition & 1 deletion src/app/(main)/(secondary)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PropsWithChildren } from 'react';

export default function Layout({ children }: PropsWithChildren) {
return <div className="mx-auto max-w-screen-md py-8">{children}</div>;
return <div className="mx-auto w-full max-w-screen-md py-8">{children}</div>;
}
107 changes: 107 additions & 0 deletions src/app/(main)/(secondary)/search/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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) {
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 = {
...versionsJson,
artistName: artistJson.name,
artistSlug: artistJson.slug,
};
}

return (
<div className="mx-auto w-full max-w-screen-md flex-1">
<SearchBar resultsType={resultsType} />
<div className="flex justify-between pb-4">
{data !== null && resultsType === 'versions' && (
<div className="font-semibold">
Showing all versions of “{versionsData?.name}” by {versionsData?.artistName}
</div>
)}
{data !== null && resultsType !== 'versions' && (
<ul className="search-filters">
<li>
<SearchFilterPill
buttonType="all"
resultsType={resultsType}
searchParams={searchParams}
>
All {data && `(${data?.Artists?.length + data?.Songs?.length})`}
</SearchFilterPill>
</li>
<li>
<SearchFilterPill
buttonType="artists"
resultsType={resultsType}
searchParams={searchParams}
>
Artists {data && `(${data?.Artists?.length})`}
</SearchFilterPill>
</li>
<li>
<SearchFilterPill
buttonType="songs"
resultsType={resultsType}
searchParams={searchParams}
>
Songs {data && `(${data?.Songs?.length})`}
</SearchFilterPill>
</li>
</ul>
)}
{resultsType === 'versions' && (
<SimplePopover
content={<SearchSortByMenu resultsType={resultsType} searchParams={searchParams} />}
position="bottom-end"
>
<button>Sort by ⏷</button>
</SimplePopover>
)}
</div>
<SearchResultsCpt
data={data}
resultsType={resultsType}
searchParams={searchParams}
versionsData={versionsData}
/>
</div>
);
}

export const metadata = {
title: 'Search',
};
5 changes: 5 additions & 0 deletions src/app/(main)/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,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 2xl: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
69 changes: 69 additions & 0 deletions src/components/search/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'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<string>(searchParams?.get('q')?.toString() || '');
const [isPending, startTransition] = useTransition();

function handleSubmit(e?: FormEvent<HTMLFormElement>) {
if (e) {
e.preventDefault();
}

if (value) {
writableParams.set('q', value);
} else {
writableParams.delete('q');
}

startTransition(() => router.replace(`${pathname}?${writableParams.toString()}`));
}

function clearSearch() {
writableParams.delete('artistUuid');
writableParams.delete('q');
writableParams.delete('resultsType');
writableParams.delete('songUuid');
writableParams.delete('sortBy');
startTransition(() => {
setValue('');
router.replace(`${pathname}?${writableParams.toString()}`);
});
}

return (
<form className="w-screen max-w-screen-md" onSubmit={handleSubmit}>
{resultsType === 'versions' ? (
<button
className="mb-2 h-[42px] font-semibold"
onClick={clearSearch}
aria-label="clear search"
type="button" // Technically a reset button, but maybe best to use "button" to avoid unexpected behavior
>
<i className="fa fa-times" /> Clear search
</button>
) : (
<div className="search-bar mb-2 flex items-center p-2">
<i className={isPending ? 'fa fa-spinner px-2' : 'fa fa-search px-2'} />
<input
className="grow"
type="text"
placeholder="Search..."
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button onClick={clearSearch} aria-label="clear search" className="flex" type="button">
<i className="fa fa-times px-2" />
</button>
</div>
)}
</form>
);
}
29 changes: 29 additions & 0 deletions src/components/search/SearchFilterPill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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 writableParams = new URLSearchParams(searchParams);

writableParams.set('resultsType', buttonType);

return (
<Link
className={`mr-1 mt-1 inline-block rounded-full px-2 py-1 ${resultsType === buttonType ? 'search-filters-item--active' : ''}`}
href={`search?${writableParams.toString()}`}
replace={true}
>
{children}
</Link>
);
}
Loading