Compare commits
No commits in common. "interview_start" and "main" have entirely different histories.
interview_
...
main
64
Pagination.tsx
Normal file
64
Pagination.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
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={">>"} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
23
movie-search/__tests__/MovieCard.test.jsx
Normal file
23
movie-search/__tests__/MovieCard.test.jsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
41
movie-search/__tests__/SearchBar.test.jsx
Normal file
41
movie-search/__tests__/SearchBar.test.jsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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();
|
||||||
|
// });
|
||||||
|
});
|
16
movie-search/__tests__/omdb.test.jsx
Normal file
16
movie-search/__tests__/omdb.test.jsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
19
movie-search/src/api/omdb.ts
Normal file
19
movie-search/src/api/omdb.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
};
|
12
movie-search/src/app/Favorites/favoriteList.tsx
Normal file
12
movie-search/src/app/Favorites/favoriteList.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"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} />
|
||||||
|
);
|
||||||
|
}
|
10
movie-search/src/app/Favorites/page.tsx
Normal file
10
movie-search/src/app/Favorites/page.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import FavoriteList from "./favoriteList";
|
||||||
|
|
||||||
|
export default function Favorites() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Your Favorites!</h1>
|
||||||
|
<FavoriteList/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
5
movie-search/src/app/Movie/loading.tsx
Normal file
5
movie-search/src/app/Movie/loading.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Loading from "@/common/components/Loading";
|
||||||
|
|
||||||
|
export default function loading() {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
39
movie-search/src/app/Movie/page.tsx
Normal file
39
movie-search/src/app/Movie/page.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
33
movie-search/src/app/Search/SearchBar.tsx
Normal file
33
movie-search/src/app/Search/SearchBar.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
16
movie-search/src/app/Search/SearchPage.tsx
Normal file
16
movie-search/src/app/Search/SearchPage.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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} />
|
||||||
|
);
|
||||||
|
}
|
22
movie-search/src/app/Search/page.tsx
Normal file
22
movie-search/src/app/Search/page.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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,6 +1,8 @@
|
||||||
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"] });
|
||||||
|
|
||||||
|
@ -17,7 +19,10 @@ 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>
|
||||||
);
|
);
|
||||||
|
|
34
movie-search/src/common/components/FavoriteBtn.tsx
Normal file
34
movie-search/src/common/components/FavoriteBtn.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
"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>
|
||||||
|
);
|
||||||
|
}
|
59
movie-search/src/common/components/FavoriteContext.tsx
Normal file
59
movie-search/src/common/components/FavoriteContext.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
"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>
|
||||||
|
);
|
||||||
|
}
|
41
movie-search/src/common/components/ImgCard.tsx
Normal file
41
movie-search/src/common/components/ImgCard.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
9
movie-search/src/common/components/Loading.tsx
Normal file
9
movie-search/src/common/components/Loading.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
32
movie-search/src/common/components/MovieCard.tsx
Normal file
32
movie-search/src/common/components/MovieCard.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
23
movie-search/src/common/components/MovieList.tsx
Normal file
23
movie-search/src/common/components/MovieList.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
13
movie-search/src/common/components/NavBar.tsx
Normal file
13
movie-search/src/common/components/NavBar.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
7
movie-search/src/common/utils/searchParams.ts
Normal file
7
movie-search/src/common/utils/searchParams.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
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