Skip to content

Commit

Permalink
feat/added autocomplete (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
sirLisko authored Sep 21, 2024
1 parent 84fed13 commit a156fa8
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 55 deletions.
170 changes: 157 additions & 13 deletions components/Home/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,183 @@
import React, { useRef, FormEvent } from "react";
import React, {
useState,
useRef,
useEffect,
FormEvent,
KeyboardEvent,
} from "react";
import { useRouter } from "next/router";
import { Search as SearchIcon } from "lucide-react";
import { Search as SearchIcon, X as CloseIcon } from "lucide-react";

type Artist = {
id: string;
name: string;
disambiguation?: string;
};

const Search = () => {
const [searchTerm, setSearchTerm] = useState("");
const [suggestions, setSuggestions] = useState<Artist[]>([]);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [isOpen, setIsOpen] = useState(false);
const search = useRef<HTMLInputElement>(null);
const wrapperRef = useRef<HTMLFormElement>(null);
const router = useRouter();

useEffect(() => {
const fetchSuggestions = async () => {
if (searchTerm.length > 1) {
try {
const response = await fetch(
`https://musicbrainz.org/ws/2/artist?query=${encodeURIComponent(searchTerm)}&fmt=json`,
);
const data = await response.json();
setSuggestions(data.artists.slice(0, 5));
setSelectedIndex(-1);
setIsOpen(true);
} catch (error) {
console.error("Error fetching suggestions:", error);
}
} else {
setSuggestions([]);
setSelectedIndex(-1);
setIsOpen(false);
}
};

const debounceTimer = setTimeout(fetchSuggestions, 300);
return () => clearTimeout(debounceTimer);
}, [searchTerm]);

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
wrapperRef.current &&
!wrapperRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
}

document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [wrapperRef]);

const onFormSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
search.current?.value &&
router.push("/[artist]", `/${search.current.value}`);
if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
handleSuggestionSelect(suggestions[selectedIndex]);
} else {
searchTerm && router.push("/[...artist]", `/${searchTerm}`);
}
};

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value);
};

const handleSuggestionSelect = (suggestion: Artist) => {
setSearchTerm(suggestion.name);
setSuggestions([]);
setIsOpen(false);
router.push("/[...artist]", `/${suggestion.name}/${suggestion.id}`);
};

const clearSearch = () => {
setSearchTerm("");
setSuggestions([]);
setSelectedIndex(-1);
setIsOpen(false);
if (search.current) {
search.current.focus();
}
};

const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (!isOpen) return;

switch (event.key) {
case "ArrowDown":
event.preventDefault();
setSelectedIndex((prev) =>
prev < suggestions.length - 1 ? prev + 1 : prev,
);
break;
case "ArrowUp":
event.preventDefault();
setSelectedIndex((prev) => (prev > -1 ? prev - 1 : -1));
break;
case "Enter":
event.preventDefault();
if (selectedIndex >= 0) {
handleSuggestionSelect(suggestions[selectedIndex]);
}
break;
case "Escape":
setIsOpen(false);
setSelectedIndex(-1);
break;
}
};

return (
<form className="w-full max-w-md mb-12" onSubmit={onFormSubmit}>
<form
className="w-full max-w-md mb-12 relative"
onSubmit={onFormSubmit}
ref={wrapperRef}
>
<div className="relative">
<label htmlFor="search" className="sr-only">
Search for an artist:
</label>
<input
id="search"
type="search"
type="text"
placeholder="Search an Artist"
autoComplete="off"
spellCheck="false"
autoFocus={true}
ref={search}
className="w-full py-3 px-4 pr-12 rounded-full bg-white bg-opacity-20 backdrop-blur-md text-white placeholder-white placeholder-opacity-75 focus:outline-none focus:ring-2 focus:ring-white"
value={searchTerm}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
className="w-full py-3 px-4 pr-12 rounded-full bg-white bg-opacity-20 backdrop-blur-md text-white placeholder-white placeholder-opacity-75 focus:outline-none focus:ring-2 focus:ring-white text-lg"
/>
{searchTerm && (
<button
type="button"
onClick={clearSearch}
className="absolute right-14 top-1/2 transform -translate-y-1/2 text-white opacity-75 hover:opacity-100"
>
<CloseIcon size={20} />
</button>
)}
<button
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-white"
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-white opacity-75 hover:opacity-100"
type="submit"
aria-labelledby="search"
aria-label="Search"
>
<SearchIcon />
<SearchIcon size={24} />
</button>
</div>
{isOpen && suggestions.length > 0 && (
<ul className="absolute z-10 w-full mt-1 bg-opacity-95 backdrop-blur-md rounded-2xl shadow-lg overflow-hidden border">
{suggestions.map((suggestion, index) => (
<li
key={index}
className={`px-6 py-3 cursor-pointer transition-colors duration-150 ease-in-out ${
index === selectedIndex
? "bg-blue-100 text-blue-500"
: "hover:bg-blue-50 hover:text-blue-500"
}`}
onClick={() => handleSuggestionSelect(suggestion)}
>
{suggestion.name}
{suggestion.disambiguation
? ` (${suggestion.disambiguation})`
: ""}
</li>
))}
</ul>
)}
</form>
);
};
Expand Down
15 changes: 9 additions & 6 deletions components/Result/Result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import { ArrowLeft, Frown } from "lucide-react";
import Link from "next/link";

interface Props {
artistName: string;
artist: string[];
}

const Result = ({ artistName }: Props) => {
const { artistData, isLoading: isLoadingArtist } = useArtistData(artistName);
const { tracks, isLoading: isLoadingTracks } = useTracks(artistName);
const { events } = useEvents(artistName);
const Result = ({ artist }: Props) => {
const { artistData, isLoading: isLoadingArtist } = useArtistData(artist[0]);
const { tracks, isLoading: isLoadingTracks } = useTracks(
artist[0],
artist[1],
);
const { events } = useEvents(artist[0]);

const from = `rgba(${artistData?.palette?.DarkVibrant.rgb.join(",")},100)`;

Expand Down Expand Up @@ -88,7 +91,7 @@ const Result = ({ artistName }: Props) => {
<Frown height={100} width={100} />
</div>
<div className="m-auto text-center text-2xl p-3">
No setlists found for <b>{artistName}</b>
No setlists found for <b>{artist[0]}</b>
</div>
</div>
)}
Expand Down
13 changes: 7 additions & 6 deletions pages/[artist].tsx → pages/[...artist].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import { useTracks } from "services/tracks";

const ResultPage = () => {
const router = useRouter();
const artistName = router.query.artist as string | undefined;
const { isLoading: isLoadingArtist } = useArtistData(artistName);
const { isLoading: isLoadingTracks } = useTracks(artistName);
const artist = router.query.artist as string[] | undefined;

const { isLoading: isLoadingArtist } = useArtistData(artist?.[0]);
const { isLoading: isLoadingTracks } = useTracks(artist?.[0], artist?.[1]);
const isLoading = isLoadingArtist || isLoadingTracks;
const showAlternate = isLoading || !artistName;
const showAlternate = isLoading || !artist;

return (
<main
Expand All @@ -25,12 +26,12 @@ const ResultPage = () => {
})}
>
<Head />
{isLoading || !artistName ? (
{isLoading || !artist ? (
<div className="m-auto text-center text-2xl p-3" aria-label="lo">
<Loader height={80} width={80} ariaLabel="loading" color="white" />
</div>
) : (
<Result artistName={artistName} />
<Result artist={artist} />
)}
{!isLoading && (
<Footer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { HttpStatusCode } from "axios";

export default async (req: NextApiRequest, res: NextApiResponse) => {
const { artistName } = req.query as { artistName: string };
if (!artistName) {
const { artistName, artistId } = req.query as {
artistName?: string;
artistId?: string;
};
if (!artistName && !artistId) {
return res.status(HttpStatusCode.BadRequest).end();
}
try {
const setList = await getArtistSetlist(artistName);
const setList = await getArtistSetlist(artistName, artistId);
res.status(HttpStatusCode.Ok).json(getAggregatedSetlists(setList));
} catch (e: any) {
res
Expand Down
35 changes: 25 additions & 10 deletions server/apis/setlistFm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,30 @@ const headers = {
"x-api-key": process.env.SETLISTFMAPIKEY as string,
};

export const getArtistSetlist = async (artist: string) => {
const { data: artistData } = await axios(`${DOMAIN}${ARTIST_PATH}${artist}`, {
headers,
});
const { data } = await axios(
`${DOMAIN}${SETLIST_PATH}${artistData?.artist?.[0].mbid}`,
{
export const getArtistSetlist = async (
artistName?: string,
artistId?: string,
) => {
if (artistId) {
const { data } = await axios(`${DOMAIN}${SETLIST_PATH}${artistId}`, {
headers,
}
);
return data;
});
return data;
}
if (artistName) {
const { data: artistData } = await axios(
`${DOMAIN}${ARTIST_PATH}${artistName}`,
{
headers,
},
);
const { data } = await axios(
`${DOMAIN}${SETLIST_PATH}${artistData?.artist?.[0].mbid}`,
{
headers,
},
);
return data;
}
throw new Error("No artist name or id provided");
};
8 changes: 6 additions & 2 deletions services/tracks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import useSWR from "swr";
import { Track } from "types";
import { fetcher } from "utils/api";

export const useTracks = (artist?: string) => {
export const useTracks = (artistName?: string, artistId?: string) => {
const { data, error, isLoading } = useSWR(
artist ? `/api/artists/${artist}/tracks` : null,
artistId
? `/api/tracks?artistId=${artistId}`
: artistName
? `/api/tracks?artistName=${artistName}`
: null,
fetcher<Track[]>,
{ revalidateOnFocus: false, revalidateOnReconnect: false },
);
Expand Down
20 changes: 5 additions & 15 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
Expand All @@ -20,14 +16,8 @@
"noImplicitAny": true,
"baseUrl": ".",
"incremental": true,
"useUnknownInCatchVariables": false,
"useUnknownInCatchVariables": false
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

0 comments on commit a156fa8

Please sign in to comment.