add movies creation

This commit is contained in:
qpismont 2024-07-08 21:25:31 +02:00
parent afd9170d6d
commit 9a9f66b5b9
11 changed files with 285 additions and 32 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -9,17 +9,17 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.7.0", "@astrojs/check": "^0.8.0",
"@astrojs/node": "^8.3.2", "@astrojs/node": "^8.3.2",
"@astrojs/react": "^3.6.0", "@astrojs/react": "^3.6.0",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "^4.11.3", "astro": "^4.11.5",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-icons": "^5.2.1", "react-icons": "^5.2.1",
"typescript": "^5.5.2" "typescript": "^5.5.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.8.3" "@biomejs/biome": "1.8.3"

View file

@ -3,7 +3,11 @@ import type { Movie } from "../types";
import TmdbSearchInput from "./TmdbSearchInput"; import TmdbSearchInput from "./TmdbSearchInput";
import GroupMoviesList from "./lists/movies/GroupMoviesList"; import GroupMoviesList from "./lists/movies/GroupMoviesList";
export default function TmdbSearch() { interface TmdbSearchProps {
onSearch: (movie: Movie) => void;
}
export default function TmdbSearch({ onSearch }: TmdbSearchProps) {
const [movies, setMovies] = useState<Movie[]>([]); const [movies, setMovies] = useState<Movie[]>([]);
function handleOnSearch(movies: Movie[]) { function handleOnSearch(movies: Movie[]) {
@ -19,7 +23,7 @@ export default function TmdbSearch() {
</div> </div>
<div style={{ marginTop: "8px" }} className="row"> <div style={{ marginTop: "8px" }} className="row">
<div className="col"> <div className="col">
<GroupMoviesList movies={movies} /> <GroupMoviesList movies={movies} onClick={onSearch} />
</div> </div>
</div> </div>
</> </>

View file

@ -0,0 +1,34 @@
import { useState } from "react";
import TmdbSearch from "../TmdbSearch";
import type { CreateMovie, Movie } from "../../types";
import MovieFormData from "./MovieFormData";
export default function AddMovieForm() {
const [tmdbMovie, setTmdbMovie] = useState<Movie | null>(null);
function handleOnSearch(movie: Movie) {
setTmdbMovie(movie);
}
const createMovie: CreateMovie | undefined = tmdbMovie
? {
title: tmdbMovie.title,
backdrop_path: tmdbMovie.backdrop_path,
poster_path: tmdbMovie.poster_path,
overview: tmdbMovie.overview,
release_date: tmdbMovie.release_date,
tmdb_id: tmdbMovie.id,
}
: undefined;
return (
<div className="row">
<div className="col">
<TmdbSearch onSearch={handleOnSearch} />
</div>
<div className="col">
<MovieFormData defaultValue={createMovie} />
</div>
</div>
);
}

View file

@ -7,6 +7,7 @@ export interface LoginFormValue {
interface LoginFormProps { interface LoginFormProps {
onSubmit: (data: LoginFormValue) => void; onSubmit: (data: LoginFormValue) => void;
disabled?: boolean;
} }
export default function LoginForm(props: LoginFormProps) { export default function LoginForm(props: LoginFormProps) {
@ -60,6 +61,7 @@ export default function LoginForm(props: LoginFormProps) {
<div className="d-grid"> <div className="d-grid">
<button <button
type="button" type="button"
disabled={props.disabled}
onClick={handleBtnClick} onClick={handleBtnClick}
className="btn btn-primary" className="btn btn-primary"
> >

View file

@ -1,8 +1,12 @@
import { useState } from "react";
import LoginForm, { type LoginFormValue } from "./LoginForm"; import LoginForm, { type LoginFormValue } from "./LoginForm";
export default function LoginFormData() { export default function LoginFormData() {
const [loading, setLoading] = useState<boolean>(false);
function handleOnFormSubmit(data: LoginFormValue) { function handleOnFormSubmit(data: LoginFormValue) {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
setLoading(true);
fetch("/api/accounts/login", { fetch("/api/accounts/login", {
method: "POST", method: "POST",
@ -14,11 +18,12 @@ export default function LoginFormData() {
}, },
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((json) => { .then(() => {
window.location.href = searchParams.get("redirect") || "/home"; window.location.href = searchParams.get("redirect") || "/home";
}) })
.catch((err) => console.log(err)); .catch((err) => console.log(err))
.finally(() => setLoading(false));
} }
return <LoginForm onSubmit={handleOnFormSubmit} />; return <LoginForm onSubmit={handleOnFormSubmit} disabled={loading} />;
} }

View file

@ -0,0 +1,120 @@
import { useEffect, useState } from "react";
import type { CreateMovie } from "../../types";
interface MovieFormProps {
defaultValue?: CreateMovie;
disabled?: boolean;
onSubmit: (createMovie: CreateMovie) => void;
}
export default function MovieForm({
defaultValue,
onSubmit,
disabled,
}: MovieFormProps) {
const [form, setForm] = useState<CreateMovie>(
defaultValue || {
title: "",
overview: "",
release_date: "",
backdrop_path: "",
poster_path: "",
tmdb_id: 0,
},
);
useEffect(() => {
if (defaultValue) setForm(defaultValue);
}, [defaultValue]);
function handleInputChange(
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
const target = event.target;
const name = target.name;
const value = target.value;
setForm({
...form,
[name]: value,
});
}
function handleBtnClick() {
onSubmit({ ...form });
}
return (
<div className="card">
<div className="card-body">
<form>
<div className="form-floating mb-3">
<input
type="text"
className="form-control"
id="title"
name="title"
value={form.title}
onChange={handleInputChange}
/>
<label className="form-label">Title</label>
</div>
<div className="form-floating mb-3">
<textarea
className="form-control"
id="overview"
name="overview"
style={{ minHeight: "96px" }}
onChange={handleInputChange}
value={form.overview}
/>
<label className="form-label">Overview</label>
</div>
<div className="form-floating mb-3">
<input
type="date"
className="form-control"
id="release_date"
name="release_date"
onChange={handleInputChange}
value={form.release_date}
/>
<label className="form-label">Release date</label>
</div>
<div className="form-floating mb-3">
<input
type="text"
className="form-control"
id="backdrop_path"
name="backdrop_path"
onChange={handleInputChange}
value={form.backdrop_path}
/>
<label className="form-label">Backdrop path</label>
</div>
<div className="form-floating mb-3">
<input
type="text"
className="form-control"
id="poster_path"
name="poster_path"
onChange={handleInputChange}
value={form.poster_path}
/>
<label className="form-label">Poster path</label>
</div>
</form>
<div className="d-grid">
<button
type="button"
disabled={disabled}
onClick={handleBtnClick}
className="btn btn-primary"
>
Create
</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,40 @@
import { useState } from "react";
import type { CreateMovie } from "../../types";
import MovieForm from "./MovieForm";
interface MovieFormDataProps {
defaultValue?: CreateMovie;
}
export default function MovieFormData({ defaultValue }: MovieFormDataProps) {
const [loading, setLoading] = useState<boolean>(false);
function handleOnSubmit(createMovie: CreateMovie) {
const searchParams = new URLSearchParams(window.location.search);
setLoading(true);
fetch("/api/movies", {
method: "POST",
body: JSON.stringify(createMovie),
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((movie) => {
console.log(movie);
})
.catch((err) => console.log(err))
.finally(() => setLoading(false));
}
return (
<MovieForm
defaultValue={defaultValue}
disabled={loading}
onSubmit={handleOnSubmit}
/>
);
}

View file

@ -0,0 +1,38 @@
import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request, cookies }) => {
const jwt = cookies.get("jwt")?.value as string;
const reqBody = await request.text();
if (!reqBody) throw new Error("body not found");
const res = await fetch(`${import.meta.env.API_URL}/movies`, {
method: "POST",
body: reqBody,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwt}`,
},
});
const resBody = await res.text();
return new Response(resBody, { status: res.status });
};
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}/movies/search?q=${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 });
};

View file

@ -1,4 +1,5 @@
--- ---
import AddMovieForm from "../../../components/forms/AddMovieForm";
import TmdbSearch from "../../../components/TmdbSearch"; import TmdbSearch from "../../../components/TmdbSearch";
import HomeLayout from "../../../layouts/HomeLayout.astro"; import HomeLayout from "../../../layouts/HomeLayout.astro";
--- ---
@ -12,9 +13,8 @@ import HomeLayout from "../../../layouts/HomeLayout.astro";
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h3>Search on TMDB Database</h3> <h3>Search on TMDB Database</h3>
<TmdbSearch client:load /> <AddMovieForm client:load />
</div> </div>
<div class="col"></div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,6 +2,16 @@ export interface Movie {
id: number; id: number;
title: string; title: string;
overview: string; overview: string;
backdrop_path: string;
poster_path: string; poster_path: string;
release_date: string; release_date: string;
} }
export interface CreateMovie {
title: string;
overview: string;
poster_path: string;
backdrop_path: string;
release_date: string;
tmdb_id: number;
}