minio #1

Open
qpismont wants to merge 5 commits from minio into main
38 changed files with 164 additions and 933 deletions
Showing only changes of commit de0eb13032 - Show all commits

7
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM debian:12
WORKDIR /app
RUN apt update &&\
apt install git curl unzip -y &&\
curl -fsSL https://bun.sh/install | bash

View file

@ -0,0 +1,18 @@
{
"workspaceFolder": "/workspace",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,Z",
"build": {
"dockerfile": "Dockerfile"
},
"customizations": {
"vscode": {
"extensions": [
"oven.bun-vscode",
"biomejs.biome",
"astro-build.astro-vscode"
]
}
},
"forwardPorts": [8080],
"runArgs": ["--network=dev-network", "--name=trepa-web-dev"]
}

View file

@ -10,4 +10,7 @@ export default defineConfig({
adapter: node({ adapter: node({
mode: "standalone", mode: "standalone",
}), }),
server: {
port: 8080
}
}); });

View file

@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": { "organizeImports": {
"enabled": true "enabled": true
}, },

BIN
bun.lockb

Binary file not shown.

View file

@ -9,19 +9,19 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.8.2", "@astrojs/check": "^0.9.4",
"@astrojs/node": "^8.3.2", "@astrojs/node": "^9.1.3",
"@astrojs/react": "^3.6.0", "@astrojs/react": "^4.2.2",
"@types/react": "^18.3.3", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "^4.12.2", "astro": "^5.5.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.5.0",
"typescript": "^5.5.4" "typescript": "^5.8.2"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.8.3" "@biomejs/biome": "1.9.4"
} }
} }

View file

@ -1,59 +0,0 @@
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,
);
if (props.onChange) {
props.onChange(e);
}
}
const inputAttr = {
...props,
debouceDelay: undefined,
onDebounce: undefined,
};
return (
<input
className={props.className}
placeholder={props.placeholder}
id={props.id}
type="text"
value={value}
onChange={handleOnChange}
onKeyDown={props.onKeyDown}
/>
);
}

View file

@ -1,37 +0,0 @@
import { useState } from "react";
import DebounceInput from "./DebouceInput";
interface MoviesSearchInput {
searchUrl: string;
}
export default function MoviesSearchInput({ searchUrl }: MoviesSearchInput) {
const [value, setValue] = useState<string>("");
function handleOnDebounce(value: string) {}
function handleOnChange(event: React.ChangeEvent<HTMLInputElement>) {
setValue(event.target.value);
}
function handleOnKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
window.location.href = `${searchUrl}?query=${value}`;
}
}
return (
<div className="form-floating mb-3">
<DebounceInput
debouceDelay={1000}
type="text"
className="form-control"
id="moviesearch"
onChange={handleOnChange}
onDebounce={handleOnDebounce}
onKeyDown={handleOnKeyDown}
/>
<label className="form-label">Search a movie</label>
</div>
);
}

View file

@ -1,31 +0,0 @@
import { useState } from "react";
import type { Movie } from "../types";
import TmdbSearchInput from "./TmdbSearchInput";
import GroupMoviesList from "./lists/movies/GroupMoviesList";
interface TmdbSearchProps {
onSearch: (movie: Movie) => void;
}
export default function TmdbSearch({ onSearch }: TmdbSearchProps) {
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} onClick={onSearch} />
</div>
</div>
</>
);
}

View file

@ -1,34 +0,0 @@
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}
/>
);
}

View file

@ -1,34 +0,0 @@
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

@ -1,74 +0,0 @@
import { useState } from "react";
export interface LoginFormValue {
username: string;
password: string;
}
interface LoginFormProps {
onSubmit: (data: LoginFormValue) => void;
disabled?: boolean;
}
export default function LoginForm(props: LoginFormProps) {
const [form, setForm] = useState<LoginFormValue>({
username: "",
password: "",
});
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const target = event.target;
const name = target.name;
const value = target.value;
setForm({
...form,
[name]: value,
});
}
function handleBtnClick() {
props.onSubmit({ ...form });
}
return (
<div className="card">
<div className="card-body">
<form>
<div className="form-floating mb-3">
<input
type="text"
className="form-control"
id="username"
name="username"
value={form.username}
onChange={handleInputChange}
/>
<label className="form-label">Nom d'utilisateur</label>
</div>
<div className="form-floating mb-3">
<input
type="password"
className="form-control"
id="password"
name="password"
onChange={handleInputChange}
value={form.password}
/>
<label className="form-label">Mot de passe</label>
</div>
</form>
<div className="d-grid">
<button
type="button"
disabled={props.disabled}
onClick={handleBtnClick}
className="btn btn-primary"
>
Connexion
</button>
</div>
</div>
</div>
);
}

View file

@ -1,29 +0,0 @@
import { useState } from "react";
import LoginForm, { type LoginFormValue } from "./LoginForm";
export default function LoginFormData() {
const [loading, setLoading] = useState<boolean>(false);
function handleOnFormSubmit(data: LoginFormValue) {
const searchParams = new URLSearchParams(window.location.search);
setLoading(true);
fetch("/api/accounts/login", {
method: "POST",
body: JSON.stringify(data),
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then(() => {
window.location.href = searchParams.get("redirect") || "/home";
})
.catch((err) => console.log(err))
.finally(() => setLoading(false));
}
return <LoginForm onSubmit={handleOnFormSubmit} disabled={loading} />;
}

View file

@ -1,120 +0,0 @@
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

@ -1,82 +0,0 @@
import { useState } from "react";
import type { CreateMovie } from "../../types";
import MovieForm from "./MovieForm";
declare let bootstrap: any;
interface MovieFormDataProps {
defaultValue?: CreateMovie;
}
export default function MovieFormData({ defaultValue }: MovieFormDataProps) {
const [loading, setLoading] = useState<boolean>(false);
const [showModal, setShowModal] = 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) => {
const myModal = new bootstrap.Modal(
document.getElementById("exampleModal"),
);
myModal?.show();
console.log(movie);
})
.catch((err) => console.log(err))
.finally(() => setLoading(false));
}
return (
<>
<MovieForm
defaultValue={defaultValue}
disabled={loading}
onSubmit={handleOnSubmit}
/>
<div className="modal fade" id="exampleModal" role="dialog">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Modal title</h5>
<button
type="button"
className="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div className="modal-body">
<p>Modal body text goes here.</p>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-primary">
Save changes
</button>
<button
type="button"
className="btn btn-secondary"
data-dismiss="modal"
>
Close
</button>
</div>
</div>
</div>
</div>
</>
);
}

View file

@ -0,0 +1,44 @@
---
interface Props {
error: string | null;
}
const { error } = Astro.props as Props;
---
<div class="card">
<div class="card-body">
{
error && (
<div class="alert alert-danger" style="text-align: center">
{error}
</div>
)
}
<form method="post" action="/login">
<div class="form-floating mb-3">
<input
type="text"
class="form-control"
id="username"
name="username"
/>
<label class="form-label">Nom d'utilisateur</label>
</div>
<div class="form-floating mb-3">
<input
type="password"
class="form-control"
id="password"
name="password"
/>
<label class="form-label">Mot de passe</label>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
Connexion
</button>
</div>
</form>
</div>
</div>

View file

@ -0,0 +1,51 @@
---
import LoginForm from "./LoginForm.astro";
const req = Astro.request;
const isPost = req.method === "POST";
let username = "";
let password = "";
let error = null;
if (isPost) {
const form = await req.formData();
username = form.get("username")?.toString().trim() ?? "";
password = form.get("password")?.toString().trim() ?? "";
}
if (!username || !password) {
error = "Tous les champs sont requis";
} else {
try {
const res = await fetch(`${import.meta.env.API_URL}/accounts/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
if (res.status === 401) {
error = "Identifiants invalides";
} else {
error = "Une erreur est survenue";
}
} else {
const data = await res.json();
Astro.cookies.set("jwt", data.jwt, {
path: "/",
secure: true,
httpOnly: true,
});
return Astro.redirect("/");
}
} catch (err) {
error = "Une erreur est survenue";
}
}
---
<LoginForm error={error} />

View file

@ -1,18 +0,0 @@
import type { MovieDetail } from "../../../types";
import CardMoviesListItem from "./CardMoviesListItem";
interface CardMoviesListProps {
movies: MovieDetail[];
}
export default function CardMoviesList({ movies }: CardMoviesListProps) {
const items = movies.map((elt) => {
return (
<div key={elt.movie.id} className="col-2 mb-4">
<CardMoviesListItem movie={elt} />
</div>
);
});
return <div className="row">{items}</div>;
}

View file

@ -1,61 +0,0 @@
import { FaEye } from "react-icons/fa";
import type { Movie, MovieDetail } from "../../../types";
interface CardMoviesListItemProps {
movie: MovieDetail;
}
export default function CardMoviesListItem({ movie }: CardMoviesListItemProps) {
return (
<div
className="cardshadow"
style={{
backgroundImage: `url(${movie.movie.poster_path})`,
backgroundPosition: "center",
backgroundSize: "cover",
height: "320px",
borderRadius: "5px",
}}
>
<div style={{ height: "100%", width: "100%" }} className="movieitem">
<div style={{ height: "100%" }} className="row justify-content-center">
<div style={{ height: "100%" }} className="col align-self-center">
<div style={{ height: "50%" }} className="row">
<div className="col align-self-end">
<h5 style={{ textAlign: "center", verticalAlign: "center" }}>
{movie.movie.title}
</h5>
</div>
</div>
<div
style={{ height: "50%" }}
className="row justify-content-center"
>
<div className="col-8 align-self-center">
<div className="d-grid">
{movie.stats ? (
<span className="badge text-bg-success mb-2">
Disponible
</span>
) : (
<span className="badge text-bg-warning mb-2">
Bientot dispo.
</span>
)}
<br />
<a
href={`/home/movies/${movie.movie.id}`}
className="btn btn-primary"
>
<FaEye />
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,18 +0,0 @@
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>;
}

View file

@ -1,43 +0,0 @@
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>
);
}

1
src/env.d.ts vendored
View file

@ -1,3 +1,4 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />
declare namespace App { declare namespace App {

View file

@ -9,9 +9,14 @@ const { title } = Astro.props;
--- ---
<RootLayout title={title}> <RootLayout title={title}>
<div style="height: 100%;" class="row justify-content-center"> <div
<div class="col-auto align-self-center"> class="container-fluid d-flex align-items-center justify-content-center"
<slot /> style="min-height: 100vh;"
>
<div class="row">
<div class="col-auto">
<slot />
</div>
</div> </div>
</div> </div>
</RootLayout> </RootLayout>

View file

@ -20,18 +20,3 @@ const { title } = Astro.props;
</div> </div>
</div> </div>
</RootLayout> </RootLayout>
<script>
const logoutElt = document.getElementById("logout-btn");
logoutElt?.addEventListener("click", () => {
fetch(`${import.meta.env.PUBLIC_API_URL}/accounts/logout`, {
method: "POST",
credentials: "include",
})
.then(() => {
window.location.href = "/";
})
.catch((err) => console.error(err));
});
</script>

View file

@ -1,4 +1,10 @@
--- ---
const query = Astro.url.searchParams;
if (query.get("logout")) {
Astro.cookies.delete("jwt");
return Astro.redirect("/login");
}
interface Props { interface Props {
title: string; title: string;
} }

View file

@ -3,12 +3,14 @@ import { defineMiddleware, sequence } from "astro:middleware";
const protectedRoutes = defineMiddleware(async (ctx, next) => { const protectedRoutes = defineMiddleware(async (ctx, next) => {
const pathname = ctx.url.pathname; const pathname = ctx.url.pathname;
if ( if (
!["/", "/register", "/logout"].includes(pathname) && !["/login", "/register"].includes(pathname) &&
!pathname.startsWith("/api") !pathname.startsWith("/api")
) { ) {
const redirect = `/login?redirect=${pathname}`;
const jwt = ctx.cookies.get("jwt"); const jwt = ctx.cookies.get("jwt");
if (!jwt) { if (!jwt) {
return ctx.redirect(`/?redirect=${pathname}`); return ctx.redirect(redirect);
} }
const jwtValue = jwt.value; const jwtValue = jwt.value;
@ -20,7 +22,7 @@ const protectedRoutes = defineMiddleware(async (ctx, next) => {
}); });
if (res.status !== 200) { if (res.status !== 200) {
return ctx.redirect(`/?redirect=${pathname}`); return ctx.redirect(redirect);
} }
ctx.locals.account = JSON.parse(await res.text()).account; ctx.locals.account = JSON.parse(await res.text()).account;

View file

@ -1,28 +0,0 @@
import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request, cookies }) => {
const reqBody = await request.text();
if (!reqBody) throw new Error("body not found");
const res = await fetch(`${import.meta.env.API_URL}/accounts/login`, {
method: "POST",
body: reqBody,
headers: {
"Content-Type": "application/json",
},
});
const resBody = await res.text();
const resBodyJson = JSON.parse(resBody);
if (res.status === 201 && resBodyJson?.jwt) {
cookies.set("jwt", resBodyJson.jwt, {
secure: true,
httpOnly: true,
path: "/",
sameSite: "strict",
});
}
return new Response(resBody, { status: res.status });
};

View file

@ -1,38 +0,0 @@
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,20 +0,0 @@
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 });
};

View file

@ -1,46 +0,0 @@
---
import MoviesSearchInput from "../../components/MoviesSearchInput";
import CardMoviesList from "../../components/lists/movies/CardMoviesList";
import HomeLayout from "../../layouts/HomeLayout.astro";
import type { MovieDetail } from "../../types";
const jwt = Astro.cookies.get("jwt")?.value as string;
const url = new URL(Astro.request.url);
const query = url.searchParams.get("query");
const limit = url.searchParams.get("limit") ?? "18";
const queryParams = new URLSearchParams({
limit: limit,
});
if (query) {
queryParams.append("query", query);
}
const res = await fetch(
`${import.meta.env.API_URL}/movies?${queryParams.toString()}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwt}`,
},
},
);
const resBody = (await res.json()) as { movies: MovieDetail[] };
---
<HomeLayout title="Movies">
<div class="row">
<div class="col">
<MoviesSearchInput searchUrl="/home" client:load />
</div>
</div>
<div class="row">
<div class="col">
<CardMoviesList movies={resBody?.movies || []} />
</div>
</div>
</HomeLayout>

View file

@ -1,68 +0,0 @@
---
import HomeLayout from "../../../layouts/HomeLayout.astro";
import type { Movie, MovieDetail } from "../../../types";
const jwt = Astro.cookies.get("jwt")?.value as string;
const { id } = Astro.params;
const res = await fetch(`${import.meta.env.API_URL}/movies/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwt}`,
},
});
const item = (await res.json()).movie as MovieDetail;
---
<HomeLayout title={item.movie.title}>
<>
<h1>{item.movie.title}</h1>
<div class="card">
<div
class="card-body"
style={{
background: `linear-gradient(to left, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 1), rgba(0, 0, 0, 1)), url(${item.movie.backdrop_path})`,
backgroundPosition: "center",
backgroundSize: "cover",
minHeight: "512px",
}}
>
<div class="row" style={{ height: "512px" }}>
<div class="col align-self-center">
<p>{item.movie.overview}</p>
</div>
<div class="col align-self-center">
<div class="d-grid">
{
item.stats ? (
<a
href={`/home/movies/${item.movie.id}`}
class="btn btn-primary"
>
Watch
</a>
) : (
<span class="badge text-bg-warning">
Bientot disponible
</span>
)
}
</div>
<hr />
<div class="d-grid">
<a
href={`/home/movies/${item.movie.id}`}
class="btn btn-secondary"
>
Share
</a>
</div>
</div>
</div>
</div>
</div>
</>
</HomeLayout>

View file

@ -1,34 +0,0 @@
---
import {
MdAccountBox,
MdAdminPanelSettings,
MdOutlineLocalMovies,
} from "react-icons/md";
import HomeLayout from "../../../layouts/HomeLayout.astro";
---
<HomeLayout title="Settings">
<div class="row justify-content-center">
<div class="col-4">
<div class="list-group">
<a
href="/home/settings/account"
class="list-group-item list-group-item-action"
>
<MdAccountBox /> My account
</a>
<a
href="/home/settings/accounts"
class="list-group-item list-group-item-action"
><MdAdminPanelSettings /> Accounts</a
>
<a
href="/home/settings/movies"
class="list-group-item list-group-item-action"
><MdOutlineLocalMovies /> Add movie</a
>
</div>
</div>
</div>
</HomeLayout>

View file

@ -1,20 +0,0 @@
---
import AddMovieForm from "../../../components/forms/AddMovieForm";
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>
<AddMovieForm client:load />
</div>
</div>
</div>
</div>
</HomeLayout>

View file

@ -1,8 +1,7 @@
--- ---
import LoginFormData from "../components/forms/LoginFormData"; import HomeLayout from "../layouts/HomeLayout.astro";
import EmptyLayout from "../layouts/EmptyLayout.astro";
--- ---
<EmptyLayout title="Trepa - Connexion"> <HomeLayout title="Accueil">
<LoginFormData client:load /> <h1>Accueil</h1>
</EmptyLayout> </HomeLayout>

8
src/pages/login.astro Normal file
View file

@ -0,0 +1,8 @@
---
import LoginFormData from "../components/forms/login/LoginFormData.astro";
import EmptyLayout from "../layouts/EmptyLayout.astro";
---
<EmptyLayout title="Trepa - Connexion">
<LoginFormData />
</EmptyLayout>

View file

@ -1,4 +0,0 @@
---
Astro.cookies.delete("jwt");
return Astro.redirect("/");
---