Compare commits
1 Commits
main
...
interview_
Author | SHA1 | Date | |
---|---|---|---|
|
aa0f51f564 |
|
@ -1,64 +0,0 @@
|
||||||
import { ReactEventHandler, useState } from "react";
|
|
||||||
export default function Pagination({ pages, onPageChange, currentPage }: any) {
|
|
||||||
const [page, setPage] = useState(currentPage);
|
|
||||||
const handleClick = (e: any) => {
|
|
||||||
const { value } = e.target;
|
|
||||||
let newPage;
|
|
||||||
|
|
||||||
if (isNaN(parseInt(value))) {
|
|
||||||
switch (value) {
|
|
||||||
case "<<":
|
|
||||||
// goto start
|
|
||||||
newPage = 0;
|
|
||||||
break;
|
|
||||||
case "<":
|
|
||||||
newPage = page - 1;
|
|
||||||
break;
|
|
||||||
case ">":
|
|
||||||
newPage = page + 1;
|
|
||||||
break;
|
|
||||||
case ">>":
|
|
||||||
//goto end
|
|
||||||
newPage = pages.length - 1;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newPage = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPage(newPage);
|
|
||||||
onPageChange(newPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// onPageChange(page);
|
|
||||||
// }, [page]);
|
|
||||||
|
|
||||||
const QuickLink = ({ value }: { value: string }) => {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
style={{ fontWeight: value == page ? "bold" : "normal" }}
|
|
||||||
// value={value}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<QuickLink value={"<<"} />
|
|
||||||
<QuickLink value={"<"} />
|
|
||||||
{pages.map((p: number) => (
|
|
||||||
<QuickLink value={p.toString()} />
|
|
||||||
))}
|
|
||||||
<QuickLink value={">"} />
|
|
||||||
<QuickLink value={">>"} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import MovieCard from "../src/common/components/MovieCard.tsx";
|
|
||||||
const movie = {
|
|
||||||
Title: "Dune",
|
|
||||||
Year: "2000",
|
|
||||||
imdbID: "tt0142032",
|
|
||||||
Type: "series",
|
|
||||||
Poster:
|
|
||||||
"https://m.media-amazon.com/images/M/MV5BMTU4MjMyMTkxN15BMl5BanBnXkFtZTYwODA5OTU5._V1_SX300.jpg",
|
|
||||||
};
|
|
||||||
describe("MovieCard Rendering", () => {
|
|
||||||
test("should render with Dune title.", () => {
|
|
||||||
render(<MovieCard movie={movie} />);
|
|
||||||
const movieTitle = screen.getByRole("heading");
|
|
||||||
expect(movieTitle).toHaveTextContent("Dune");
|
|
||||||
});
|
|
||||||
test("should render with Year 2000.", () => {
|
|
||||||
render(<MovieCard movie={movie} />);
|
|
||||||
const movieTitle = screen.getByTestId("MovieCard-Year");
|
|
||||||
expect(movieTitle).toHaveTextContent("2000");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,41 +0,0 @@
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
|
||||||
import SearchBar from "@/app/Search/SearchBar.tsx";
|
|
||||||
import { queryOMDb } from "@/api/omdb";
|
|
||||||
|
|
||||||
describe("SearchBar Operations", () => {
|
|
||||||
test("SearchBar Allows user input", () => {
|
|
||||||
render(<SearchBar />);
|
|
||||||
const input = screen.getByPlaceholderText("Search by Title");
|
|
||||||
|
|
||||||
fireEvent.change(input, { target: { value: "some value" } });
|
|
||||||
|
|
||||||
expect(input.value).toBe("some value");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("SearchBar Allows user input and Clear button clears", () => {
|
|
||||||
render(<SearchBar />);
|
|
||||||
const input = screen.getByTestId("SearchBar-input");
|
|
||||||
const clear = screen.getByText("Clear");
|
|
||||||
|
|
||||||
fireEvent.change(input, { target: { value: "some value" } });
|
|
||||||
expect(input.value).toBe("some value");
|
|
||||||
|
|
||||||
fireEvent.click(clear);
|
|
||||||
|
|
||||||
expect(input.value).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
// test("SearchBar Allows user submit", () => {
|
|
||||||
// render(<SearchBar />);
|
|
||||||
// const input = screen.getByPlaceholderText("Search by Title");
|
|
||||||
// const submit = screen.getByText("Search");
|
|
||||||
// const spy = jest.spyOn({ queryOMDb }, "queryOMDb");
|
|
||||||
|
|
||||||
// fireEvent.change(input, { target: { value: "back to the" } });
|
|
||||||
// fireEvent.click(submit);
|
|
||||||
// // Somehow test for form submission - currently facing this issue
|
|
||||||
// // https://github.com/vercel/next.js/issues/54757
|
|
||||||
// // expect(spy).toHaveBeenCalled();
|
|
||||||
// });
|
|
||||||
});
|
|
|
@ -1,16 +0,0 @@
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import { queryOMDb } from "../src/api/omdb.ts";
|
|
||||||
|
|
||||||
describe("OMDb API Integration", () => {
|
|
||||||
test("queryOMDb returns movie data successfully", async () => {
|
|
||||||
const data = await queryOMDb("s=back to");
|
|
||||||
|
|
||||||
expect(data).toHaveProperty("Response", "True");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("queryOMDb returns movie with error", async () => {
|
|
||||||
const data = await queryOMDb("f=dune");
|
|
||||||
|
|
||||||
expect(data).toHaveProperty("Response", "False");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,19 +0,0 @@
|
||||||
const url = `${process.env.OMDB_URL}?apikey=${process.env.API_KEY}`;
|
|
||||||
|
|
||||||
export const queryOMDb = async (queryParamString: string) => {
|
|
||||||
"use server";
|
|
||||||
try {
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
const queryUrl = `${url}&${queryParamString}`;
|
|
||||||
const response = await fetch(queryUrl);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch movie data");
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
// console.log(queryUrl, result);
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error("Something went wrong searching OMDb");
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,12 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { FavoriteContext } from "@/common/components/FavoriteContext";
|
|
||||||
import MovieList from "@/common/components/MovieList";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export default function FavoriteList() {
|
|
||||||
const { favorites } = useContext(FavoriteContext);
|
|
||||||
return (
|
|
||||||
<MovieList movies={favorites} />
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import FavoriteList from "./favoriteList";
|
|
||||||
|
|
||||||
export default function Favorites() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Your Favorites!</h1>
|
|
||||||
<FavoriteList/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,5 +0,0 @@
|
||||||
import Loading from "@/common/components/Loading";
|
|
||||||
|
|
||||||
export default function loading() {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { queryOMDb } from "@/api/omdb";
|
|
||||||
import { INextJsProps } from "../Search/page";
|
|
||||||
import ImgCard from "@/common/components/ImgCard";
|
|
||||||
import { stringifyQueryParams } from "@/common/utils/searchParams";
|
|
||||||
|
|
||||||
export default async function Movie({ searchParams }: INextJsProps) {
|
|
||||||
const movie = await queryOMDb(stringifyQueryParams(searchParams!));
|
|
||||||
const { Poster, Title, Ratings, ...details } = movie;
|
|
||||||
return (
|
|
||||||
<div className="rounded p-4 bg-gray-300 w-auto mx-4 flex-column h-xl">
|
|
||||||
<ImgCard
|
|
||||||
width={400}
|
|
||||||
height={580}
|
|
||||||
src={Poster !== "N/A" ? Poster : ""}
|
|
||||||
alt={`${details.Title} Movie Poster`}
|
|
||||||
>
|
|
||||||
<div className="mt-auto">
|
|
||||||
<h3 className="mb-2 text-lg font-semibold">{Title}</h3>
|
|
||||||
{Object.entries(details).map(([key, value]) => {
|
|
||||||
return (
|
|
||||||
<p key={key} className="mb-2 text-sm text-gray-700">
|
|
||||||
<strong>{key}: </strong>
|
|
||||||
{value as string}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<h3>Ratings:</h3>
|
|
||||||
<ul>
|
|
||||||
{Ratings.map((r: { Source: string; Value: string }) => (
|
|
||||||
<li key={r.Source}>
|
|
||||||
{r.Source} - {r.Value}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</ImgCard>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { primaryBtn, secondaryBtn } from "@/common/classes";
|
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
export default function SearchBar() {
|
|
||||||
const searchMovies = async (formData: FormData) => {
|
|
||||||
"use server";
|
|
||||||
|
|
||||||
const movieTitle = formData.get("movieTitle");
|
|
||||||
// revalidatePath('/Search')
|
|
||||||
redirect(`/Search?s=${movieTitle}`);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<form action={searchMovies} className={`md:flex gap-4 m-4 justify-center`}>
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<input
|
|
||||||
data-testid="SearchBar-input"
|
|
||||||
className="rounded px-2 mb-2 md:mb-0"
|
|
||||||
placeholder="Search by Title"
|
|
||||||
name="movieTitle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4 justify-center">
|
|
||||||
<button type="submit" className={primaryBtn}>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
<button type="reset" className={secondaryBtn}>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { queryOMDb } from "@/api/omdb";
|
|
||||||
import MovieList from "../../common/components/MovieList";
|
|
||||||
|
|
||||||
export default async function SearchPage({
|
|
||||||
queryParamString,
|
|
||||||
}: {
|
|
||||||
queryParamString: string;
|
|
||||||
}) {
|
|
||||||
|
|
||||||
const { Search } = await queryOMDb(queryParamString);
|
|
||||||
const movies = Search || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MovieList movies={movies} />
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { stringifyQueryParams } from "@/common/utils/searchParams";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import SearchPage from "./SearchPage";
|
|
||||||
import SearchBar from "./SearchBar";
|
|
||||||
import Loading from "@/common/components/Loading";
|
|
||||||
|
|
||||||
export interface INextJsProps {
|
|
||||||
searchParams?: object;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function Search({ searchParams }: INextJsProps) {
|
|
||||||
const queryParamString = stringifyQueryParams(searchParams!);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`mx-auto px-2`}>
|
|
||||||
<SearchBar />
|
|
||||||
<Suspense key={queryParamString} fallback={<Loading />}>
|
|
||||||
<SearchPage queryParamString={queryParamString} />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,8 +1,6 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import NavBar from "@/common/components/NavBar";
|
|
||||||
import { FavoriteProvider } from "@/common/components/FavoriteContext";
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
@ -19,10 +17,7 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="h-full">
|
<html lang="en" className="h-full">
|
||||||
<body className={`${inter.className} mb-4 bg-black h-full`}>
|
<body className={`${inter.className} mb-4 bg-black h-full`}>
|
||||||
<NavBar />
|
|
||||||
<FavoriteProvider>
|
|
||||||
{children}
|
{children}
|
||||||
</FavoriteProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useContext } from "react";
|
|
||||||
import { FavoriteContext } from "./FavoriteContext";
|
|
||||||
|
|
||||||
export default function FavoriteBtn({ movie }: any) {
|
|
||||||
const { favorites,
|
|
||||||
dispatch
|
|
||||||
// setFavorites
|
|
||||||
} = useContext(FavoriteContext);
|
|
||||||
const filteredFavorites = favorites.filter((f) => f.imdbID != movie.imdbID);
|
|
||||||
const isFavorite =
|
|
||||||
favorites.filter((f) => f.imdbID == movie.imdbID).length == 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (isFavorite) {
|
|
||||||
dispatch({type:'Remove', payload: movie})
|
|
||||||
// setFavorites([...filteredFavorites]);
|
|
||||||
} else {
|
|
||||||
dispatch({type:'Add', payload: movie})
|
|
||||||
// setFavorites([...favorites, movie]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`block text-yellow-500 rounded my-auto h-8 w-8
|
|
||||||
${
|
|
||||||
isFavorite ? " bg-black" : "border-2 border-gray-300 bg-white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
★
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { PropsWithChildren, createContext, useReducer
|
|
||||||
// , useState
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
// interface IFavoriteContext {
|
|
||||||
// favorites: Array<IMovieSearch>
|
|
||||||
// setFavorites: (value: Array<IMovieSearch>) => void
|
|
||||||
// }
|
|
||||||
// export const FavoriteContext = createContext<IFavoriteContext>({
|
|
||||||
// favorites: [],
|
|
||||||
// setFavorites: (_value: Array<IMovieSearch>) => { },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Using interleaving to render this context provider client-side, but still utilize server-side renderung for children components
|
|
||||||
// export const FavoriteProvider = ({ children }: PropsWithChildren) => {
|
|
||||||
// const [favorites, setFavorites] = useState<Array<IMovieSearch>>([])
|
|
||||||
// return (
|
|
||||||
// <FavoriteContext.Provider value={{ favorites, setFavorites }}>{children}</FavoriteContext.Provider>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
interface IFavoriteContext {
|
|
||||||
favorites: FavoriteState;
|
|
||||||
dispatch: React.Dispatch<FavoriteAction>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FavoriteState = Array<IMovieSearch>;
|
|
||||||
type FavoriteAction = { type: "Add" | "Remove"; payload: IMovieSearch };
|
|
||||||
|
|
||||||
function FavoriteReducer(state: FavoriteState, action: FavoriteAction) {
|
|
||||||
switch (action.type) {
|
|
||||||
case "Add":
|
|
||||||
state = [...state, action.payload];
|
|
||||||
break;
|
|
||||||
case "Remove":
|
|
||||||
state = state.filter((s) => s.imdbID != action.payload.imdbID);
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: FavoriteState = [];
|
|
||||||
|
|
||||||
export const FavoriteContext = createContext<IFavoriteContext>({
|
|
||||||
favorites: initialState,
|
|
||||||
dispatch: () => null,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function FavoriteProvider({ children }: PropsWithChildren) {
|
|
||||||
const [favorites, dispatch] = useReducer(FavoriteReducer, initialState);
|
|
||||||
return (
|
|
||||||
<FavoriteContext.Provider value={{ favorites, dispatch }}>
|
|
||||||
{children}
|
|
||||||
</FavoriteContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import Image from "next/image";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
interface ICardProps extends PropsWithChildren {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
classNames?: string;
|
|
||||||
}
|
|
||||||
export default function ImgCard({
|
|
||||||
classNames,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
src,
|
|
||||||
alt,
|
|
||||||
children,
|
|
||||||
}: ICardProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
shadow-lg
|
|
||||||
shadow-purple-500
|
|
||||||
rounded
|
|
||||||
|
|
||||||
p-2
|
|
||||||
bg-white
|
|
||||||
text-black
|
|
||||||
${classNames}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
className={`mx-auto h-64 w-48 rounded`}
|
|
||||||
src={src || ""}
|
|
||||||
alt={alt || ""}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,9 +0,0 @@
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="h-svh flex flex-col justify-center">
|
|
||||||
<h1 className="my-auto text-center">
|
|
||||||
<strong>Loading...</strong>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import FavoriteBtn from "@/common/components/FavoriteBtn";
|
|
||||||
import ImgCard from "@/common/components/ImgCard";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { primaryBtn } from "../classes";
|
|
||||||
|
|
||||||
interface IMovieCardProps {
|
|
||||||
movie: IMovieSearch;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MovieCard({ movie }: IMovieCardProps) {
|
|
||||||
return (
|
|
||||||
<ImgCard
|
|
||||||
classNames={`w-full h-full flex flex-col justify-between`}
|
|
||||||
width={140}
|
|
||||||
height={160}
|
|
||||||
src={movie.Poster !== "N/A" ? movie.Poster : ""}
|
|
||||||
alt={`${movie.Title} Movie Poster`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<h3 className=" text-lg font-semibold">{movie.Title}</h3>
|
|
||||||
<p data-testid='MovieCard-Year' className=" text-sm text-gray-700">Circa: {movie.Year}</p>
|
|
||||||
|
|
||||||
<div className="flex gap-3 justify-around">
|
|
||||||
<Link href={`/Movie?i=${movie.imdbID}`}>
|
|
||||||
<button className={primaryBtn}>Movie Details</button>
|
|
||||||
</Link>
|
|
||||||
<FavoriteBtn movie={movie} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ImgCard>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import MovieCard from "./MovieCard";
|
|
||||||
|
|
||||||
export interface IMovieListProps {
|
|
||||||
movies: Array<IMovieSearch>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MovieList({ movies }: IMovieListProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`p-1 flex justify-around flex-wrap gap-4 sm:justify-center `}
|
|
||||||
>
|
|
||||||
{movies.length > 0 ? (
|
|
||||||
movies.map((m) => (
|
|
||||||
<div className="WrapItem w-full mx-5 sm:w-64 mx-1" key={m.imdbID}>
|
|
||||||
<MovieCard movie={m} />
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<h1>Nothing to show</h1>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default function NavBar() {
|
|
||||||
return (
|
|
||||||
<nav className="px-2 flex gap-4 shadow-md shadow-purple-500 mt-2 mb-4">
|
|
||||||
<Link href="/">
|
|
||||||
<strong>Home</strong>
|
|
||||||
</Link>
|
|
||||||
<Link href="/Search">Search</Link>
|
|
||||||
<Link href="/Favorites">Favorites</Link>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export const stringifyQueryParams = (searchParams: object) =>
|
|
||||||
Object.entries(searchParams)
|
|
||||||
.map(
|
|
||||||
([key, value], i, arr) =>
|
|
||||||
`${key}=${value}${i < arr.length - 1 ? "&" : ""}`
|
|
||||||
)
|
|
||||||
.toString();
|
|
Loading…
Reference in New Issue
Block a user