add tmdb search
This commit is contained in:
parent
65cccb9620
commit
afd9170d6d
9 changed files with 224 additions and 1 deletions
53
src/components/DebouceInput.tsx
Normal file
53
src/components/DebouceInput.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import type React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface DebounceProps {
|
||||
debouceDelay: number;
|
||||
onDebounce: (value: string) => void;
|
||||
}
|
||||
|
||||
type DebouceInputProps = DebounceProps &
|
||||
React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export default function DebounceInput(props: DebouceInputProps) {
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
const debounceTimeout = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof props.value === "string") {
|
||||
setValue(props.value);
|
||||
} else {
|
||||
setValue("");
|
||||
}
|
||||
}, [props.value]);
|
||||
|
||||
function handleOnChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (debounceTimeout.current) {
|
||||
clearTimeout(debounceTimeout.current);
|
||||
}
|
||||
|
||||
setValue(e.target.value);
|
||||
|
||||
debounceTimeout.current = setTimeout(
|
||||
() => props.onDebounce(e.target.value),
|
||||
props.debouceDelay,
|
||||
);
|
||||
}
|
||||
|
||||
const inputAttr = {
|
||||
...props,
|
||||
debouceDelay: undefined,
|
||||
onDebounce: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
className={props.className}
|
||||
placeholder={props.placeholder}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
);
|
||||
}
|
27
src/components/TmdbSearch.tsx
Normal file
27
src/components/TmdbSearch.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { useState } from "react";
|
||||
import type { Movie } from "../types";
|
||||
import TmdbSearchInput from "./TmdbSearchInput";
|
||||
import GroupMoviesList from "./lists/movies/GroupMoviesList";
|
||||
|
||||
export default function TmdbSearch() {
|
||||
const [movies, setMovies] = useState<Movie[]>([]);
|
||||
|
||||
function handleOnSearch(movies: Movie[]) {
|
||||
setMovies(movies);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<TmdbSearchInput onSearch={handleOnSearch} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: "8px" }} className="row">
|
||||
<div className="col">
|
||||
<GroupMoviesList movies={movies} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
34
src/components/TmdbSearchInput.tsx
Normal file
34
src/components/TmdbSearchInput.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import type { Movie } from "../types";
|
||||
import DebounceInput from "./DebouceInput";
|
||||
|
||||
interface TmdbSearchInputProps {
|
||||
onSearch: (movies: Movie[]) => void;
|
||||
}
|
||||
|
||||
export default function TmdbSearchInput({ onSearch }: TmdbSearchInputProps) {
|
||||
function handleOnSearchInputDebounce(value: string) {
|
||||
fetch(`/api/tmdb/movies/search?query=${value}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
onSearch(json.movies);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
onSearch([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<DebounceInput
|
||||
className="form-control"
|
||||
placeholder="Search here"
|
||||
onDebounce={handleOnSearchInputDebounce}
|
||||
debouceDelay={1000}
|
||||
/>
|
||||
);
|
||||
}
|
18
src/components/lists/movies/GroupMoviesList.tsx
Normal file
18
src/components/lists/movies/GroupMoviesList.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import type { Movie } from "../../../types";
|
||||
import GroupMoviesListItem from "./GroupMoviesListItem";
|
||||
|
||||
interface GroupMoviesListProps {
|
||||
movies: Movie[];
|
||||
onClick?: (movie: Movie) => void;
|
||||
}
|
||||
|
||||
export default function GroupMoviesList({
|
||||
movies,
|
||||
onClick,
|
||||
}: GroupMoviesListProps) {
|
||||
const items = movies.map((elt) => (
|
||||
<GroupMoviesListItem key={elt.id} item={elt} onClick={onClick} />
|
||||
));
|
||||
|
||||
return <div className="list-group">{items}</div>;
|
||||
}
|
43
src/components/lists/movies/GroupMoviesListItem.tsx
Normal file
43
src/components/lists/movies/GroupMoviesListItem.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type { Movie } from "../../../types";
|
||||
|
||||
interface GroupMoviesListItemProps {
|
||||
item: Movie;
|
||||
onClick?: (movie: Movie) => void;
|
||||
}
|
||||
|
||||
export default function GroupMoviesListItem({
|
||||
item,
|
||||
onClick,
|
||||
}: GroupMoviesListItemProps) {
|
||||
function handleOnClick(e: React.MouseEvent<HTMLElement>) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (onClick) onClick(item);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleOnClick}
|
||||
type="submit"
|
||||
className="list-group-item list-group-item-action flex-column align-items-start"
|
||||
>
|
||||
<div className="d-flex w-100 justify-content-between">
|
||||
<h5 className="mb-1">{item.title}</h5>
|
||||
<small>{item.release_date}</small>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-2">
|
||||
<img
|
||||
className="img-thumbnail"
|
||||
src={`https://image.tmdb.org/t/p/w500${item.poster_path}`}
|
||||
alt="lol"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-10">
|
||||
<p className="mb-1">{item.overview}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
20
src/pages/api/tmdb/movies/search.ts
Normal file
20
src/pages/api/tmdb/movies/search.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { APIRoute } from "astro";
|
||||
|
||||
export const GET: APIRoute = async ({ request, cookies }) => {
|
||||
const jwt = cookies.get("jwt")?.value as string;
|
||||
const url = new URL(request.url);
|
||||
|
||||
const res = await fetch(
|
||||
`${import.meta.env.API_URL}/tmdb/movies/search?query=${url.searchParams.get("query")}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const resBody = await res.text();
|
||||
return new Response(resBody, { status: res.status });
|
||||
};
|
|
@ -26,7 +26,7 @@ import HomeLayout from "../../../layouts/HomeLayout.astro";
|
|||
<a
|
||||
href="/home/settings/movies"
|
||||
class="list-group-item list-group-item-action"
|
||||
><MdOutlineLocalMovies /> Movies</a
|
||||
><MdOutlineLocalMovies /> Add movie</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
import TmdbSearch from "../../../components/TmdbSearch";
|
||||
import HomeLayout from "../../../layouts/HomeLayout.astro";
|
||||
---
|
||||
|
||||
<HomeLayout title="Add movie">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col"><h1>Add movie</h1></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>Search on TMDB Database</h3>
|
||||
<TmdbSearch client:load />
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HomeLayout>
|
7
src/types.ts
Normal file
7
src/types.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export interface Movie {
|
||||
id: number;
|
||||
title: string;
|
||||
overview: string;
|
||||
poster_path: string;
|
||||
release_date: string;
|
||||
}
|
Loading…
Reference in a new issue