= ({ text }) => {
setIsOpen(!isOpen)}
>
{text}
diff --git a/src/components/atoms/SearchInput/index.tsx b/src/components/atoms/SearchInput/index.tsx
index 9d1b996..b47ac49 100644
--- a/src/components/atoms/SearchInput/index.tsx
+++ b/src/components/atoms/SearchInput/index.tsx
@@ -1,18 +1,23 @@
"use client";
import { FC, useEffect, useState } from "react";
+import { IoSearch } from "react-icons/io5";
type Props = {
+ className?: string;
defaultValue?: string;
placeholder?: string;
onChange?: (value: string) => void;
debounce?: number;
+ autoFocus?: boolean;
};
export const SearchInput: FC = ({
+ className = "",
defaultValue = "",
placeholder = "Search",
onChange,
debounce = 500,
+ autoFocus = false,
}) => {
const [value, setValue] = useState(defaultValue);
@@ -25,12 +30,19 @@ export const SearchInput: FC = ({
}, [value]);
return (
- setValue(e.target.value)}
- />
+
+ setValue(e.target.value)}
+ autoFocus={autoFocus}
+ />
+
+
);
};
diff --git a/src/components/molecules/MovieList/index.tsx b/src/components/molecules/MovieList/index.tsx
index 808b123..adf4f0a 100644
--- a/src/components/molecules/MovieList/index.tsx
+++ b/src/components/molecules/MovieList/index.tsx
@@ -93,6 +93,7 @@ export const MovieList: FC = ({
{sortedMovies.map((movie) => (
{
- const [results, setResults] = useState([]);
+type Props = {
+ query: string;
+};
- const handleSearch = async (query: string) => {
- const data = await TMDB.search(query);
- setResults(data.results);
+export const SearchList: FC = ({ query }) => {
+ const [response, setResponse] = useState(null);
+ const {
+ results,
+ total_results = 0,
+ total_pages = 0,
+ page = 1,
+ } = response ?? {};
+
+ const handleSearch = async (query: string, page: number) => {
+ const data = await TMDB.search({
+ query,
+ page,
+ });
+ setResponse(data);
};
+ const handlePageChange = (page: number) => {
+ window.history.replaceState({}, "", `?s=${query}&page=${page}`);
+ window.scrollTo({ top: 0, behavior: "smooth" });
+ handleSearch(query, page);
+ };
+
+ useEffect(() => {
+ handleSearch(query, page);
+ }, [query]);
+
return (
-
+
+ {total_results} movies found for your search
+
- {results.map((result) => (
+ {results?.map((result) => (
{
/>
))}
+
);
diff --git a/src/components/organisms/Navbar/components/Search/index.tsx b/src/components/organisms/Navbar/components/Search/index.tsx
new file mode 100644
index 0000000..46af2c7
--- /dev/null
+++ b/src/components/organisms/Navbar/components/Search/index.tsx
@@ -0,0 +1,101 @@
+"use client";
+import { IoClose, IoSearch } from "react-icons/io5";
+import { useState } from "react";
+import { SearchResult } from "@/lib/tmdb/types";
+import { TMDB } from "@/lib/tmdb";
+import { SearchInput } from "@/components/atoms/SearchInput";
+import { MovieCard } from "@/components/atoms/MovieCard";
+import { useKeyListener } from "@/hooks/useKeyListener";
+import { useRef } from "react";
+import { useOutsideClick } from "@/hooks/useOutsideClick";
+import { Button } from "@/components/atoms/Button";
+
+export const Search = () => {
+ const ref = useRef(null);
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
+ const [response, setResponse] = useState(null);
+ const [query, setQuery] = useState("");
+ const { results, total_pages, total_results = 0 } = response ?? {};
+
+ const handleSearch = async (query: string) => {
+ setQuery(query);
+ if (query.length < 3) {
+ setResponse(null);
+ return;
+ }
+
+ const data = await TMDB.search({
+ query,
+ });
+ setResponse(data);
+ };
+
+ const handleClose = () => {
+ setIsSearchOpen(false);
+ setResponse(null);
+ };
+
+ useKeyListener("Escape", handleClose);
+ useOutsideClick(ref, handleClose);
+
+ return (
+ <>
+
+
+ {isSearchOpen && (
+
+
+
+
+
+
+
+
+ {results && (
+
+
{total_results} movies found
+
+ )}
+ {results?.slice(0, 5).map((result) => (
+
+ ))}
+ {total_results > 5 && (
+
+
+
+ )}
+
+
+
+ )}
+ >
+ );
+};
diff --git a/src/components/organisms/Navbar/index.tsx b/src/components/organisms/Navbar/index.tsx
index 886d47c..ad79de0 100644
--- a/src/components/organisms/Navbar/index.tsx
+++ b/src/components/organisms/Navbar/index.tsx
@@ -1,13 +1,74 @@
+import Link from "next/link";
+import { Search } from "./components/Search";
+
+const links = [
+ {
+ label: "My wall",
+ href: "/",
+ },
+ {
+ label: "Discover",
+ href: "/discover",
+ },
+];
+
export const Navbar = () => {
return (
-
+
-

+
+
+
Home
+

+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/src/hooks/useKeyListener/index.ts b/src/hooks/useKeyListener/index.ts
new file mode 100644
index 0000000..b54217d
--- /dev/null
+++ b/src/hooks/useKeyListener/index.ts
@@ -0,0 +1,17 @@
+import { useEffect } from "react";
+
+export const useKeyListener = (key: string, callback: () => void) => {
+ useEffect(() => {
+ const handleKey = (event: KeyboardEvent) => {
+ if (event.key === key) {
+ callback();
+ }
+ };
+
+ window.addEventListener("keydown", handleKey);
+
+ return () => {
+ window.removeEventListener("keydown", handleKey);
+ };
+ }, [key]);
+};
diff --git a/src/hooks/useLocalStorage/index.ts b/src/hooks/useLocalStorage/index.ts
new file mode 100644
index 0000000..011e43c
--- /dev/null
+++ b/src/hooks/useLocalStorage/index.ts
@@ -0,0 +1,16 @@
+import { useEffect, useState } from "react";
+
+export const useLocalStorage = (key: string, initialValue: T) => {
+ const [value, setValue] = useState(initialValue);
+
+ useEffect(() => {
+ const item = localStorage.getItem(key);
+ if (item) setValue(JSON.parse(item));
+ }, [key]);
+
+ useEffect(() => {
+ localStorage.setItem(key, JSON.stringify(value));
+ }, [value, key]);
+
+ return [value, setValue] as const;
+};
diff --git a/src/hooks/useOutsideClick/index.ts b/src/hooks/useOutsideClick/index.ts
new file mode 100644
index 0000000..a07fd61
--- /dev/null
+++ b/src/hooks/useOutsideClick/index.ts
@@ -0,0 +1,20 @@
+import { useEffect } from "react";
+
+export const useOutsideClick = (
+ ref: React.RefObject,
+ callback: () => void
+) => {
+ useEffect(() => {
+ const handleClick = (event: MouseEvent) => {
+ if (ref.current && !ref.current.contains(event.target as Node)) {
+ callback();
+ }
+ };
+
+ document.addEventListener("click", handleClick);
+
+ return () => {
+ document.removeEventListener("click", handleClick);
+ };
+ }, [ref, callback]);
+};
diff --git a/src/lib/tmdb/server.ts b/src/lib/tmdb/server.ts
index 2e91dd6..d3cab35 100644
--- a/src/lib/tmdb/server.ts
+++ b/src/lib/tmdb/server.ts
@@ -2,8 +2,9 @@
import { SearchResult } from "./types";
-const fetchTmbd = async (url: string) => {
- const response = await fetch(url, {
+const url = "https://api.themoviedb.org/3";
+const fetchTmbd = async (path: string) => {
+ const response = await fetch(url + path, {
headers: {
Authorization: `Bearer ${process.env.TMDB_BEARER}`,
},
@@ -12,8 +13,26 @@ const fetchTmbd = async (url: string) => {
return data;
};
-const url = "https://api.themoviedb.org/3";
+type SearchOptions = {
+ query: string;
+ include_adult?: boolean;
+ language?: string;
+ primary_release_year?: string;
+ page?: number;
+ region?: string;
+ year?: number;
+};
-export async function search(query: string): Promise {
- return await fetchTmbd(`${url}/search/movie?query=${query}`);
+export async function search(options: SearchOptions): Promise {
+ const params = new URLSearchParams();
+
+ options.language = "pl-PL";
+
+ Object.entries(options).forEach(([key, value]) => {
+ if (value) {
+ params.set(key, value.toString());
+ }
+ });
+
+ return await fetchTmbd(`/search/movie?${params.toString()}`);
}
diff --git a/src/lib/tmdb/types.ts b/src/lib/tmdb/types.ts
index 6e94fff..70b407d 100644
--- a/src/lib/tmdb/types.ts
+++ b/src/lib/tmdb/types.ts
@@ -16,4 +16,6 @@ export type SearchResult = {
vote_average: number;
vote_count: number;
}[];
+ total_pages: number;
+ total_results: number;
};