minio #1

Open
qpismont wants to merge 5 commits from minio into main
19 changed files with 837 additions and 103 deletions
Showing only changes of commit 2e3ec9765d - Show all commits

BIN
bun.lockb

Binary file not shown.

View file

@ -15,7 +15,6 @@
"@types/react": "^18.3.5", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "^5.5.5", "astro": "^5.5.5",
"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.5.0", "react-icons": "^5.5.0",

56
src/components/Logo.astro Normal file
View file

@ -0,0 +1,56 @@
---
interface Props {
class?: string;
}
const { class: className = "" } = Astro.props;
---
<div class={`logo ${className}`}>
<div class="logo-icon">
<div class="play-icon"></div>
</div>
</div>
<style>
.logo {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.logo-icon {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: var(--glass-blur);
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.logo-icon::before {
content: "";
position: absolute;
inset: 0;
background: var(--primary-gradient);
opacity: 0.5;
}
.play-icon {
width: 0;
height: 0;
border-style: solid;
border-width: 15px 0 15px 25px;
border-color: transparent transparent transparent #ffffff;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
position: relative;
z-index: 1;
transform: translateX(2px);
}
</style>

View file

@ -1,44 +1,56 @@
--- ---
import Alert from "../../ui/Alert.astro";
import Logo from "../../Logo.astro";
import Button from "../../ui/Button.astro";
import Input from "../../ui/Input.astro";
interface Props { interface Props {
error: string | null; error?: string;
username?: string;
password?: string;
} }
const { error } = Astro.props as Props; const { error, username = "", password = "" } = Astro.props;
--- ---
<div class="card"> <Logo />
<div class="card-body"> <hr />
{ {
error && ( error && (
<div class="alert alert-danger" style="text-align: center"> <Alert type="error" class="text-center">
{error} {error}
</div> </Alert>
) )
} }
<form method="post" action="/login"> <form method="post" action="/login">
<div class="form-floating mb-3"> <div class="form-group">
<input <label class="form-label" for="username">Nom d'utilisateur</label>
<Input
type="text" type="text"
class="form-control"
id="username" id="username"
name="username" name="username"
value={username}
placeholder="Entrez votre nom d'utilisateur"
/> />
<label class="form-label">Nom d'utilisateur</label>
</div> </div>
<div class="form-floating mb-3"> <div class="form-group">
<input <label class="form-label" for="password">Mot de passe</label>
<Input
type="password" type="password"
class="form-control"
id="password" id="password"
name="password" name="password"
value={password}
placeholder="Entrez votre mot de passe"
/> />
<label class="form-label">Mot de passe</label>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
Connexion
</button>
</div> </div>
<Button type="submit" variant="primary" class="w-100"> Connexion </Button>
</form> </form>
</div>
</div> <style>
h2 {
background: var(--primary-gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-size: 1.75rem;
}
</style>

View file

@ -1,22 +1,36 @@
--- ---
import LoginForm from "./LoginForm.astro"; import LoginForm from "./LoginForm.astro";
const req = Astro.request; type LoginError =
const isPost = req.method === "POST"; | "MISSING_FIELDS"
| "INVALID_CREDENTIALS"
| "SERVER_ERROR"
| "NOT_FOUND";
let username = ""; const ERROR_MESSAGES: Record<LoginError, string> = {
let password = ""; MISSING_FIELDS: "Tous les champs sont requis",
let error = null; INVALID_CREDENTIALS: "Identifiants invalides",
NOT_FOUND: "Compte non trouvé",
SERVER_ERROR: "Une erreur est survenue",
};
if (isPost) { interface LoginResponse {
const form = await req.formData(); jwt: string;
username = form.get("username")?.toString().trim() ?? "";
password = form.get("password")?.toString().trim() ?? "";
} }
interface LoginResult {
error?: LoginError;
jwt?: string;
}
async function handleLogin(
username: string,
password: string,
): Promise<LoginResult> {
if (!username || !password) { if (!username || !password) {
error = "Tous les champs sont requis"; return { error: "MISSING_FIELDS" };
} else { }
try { try {
const res = await fetch(`${import.meta.env.API_URL}/accounts/login`, { const res = await fetch(`${import.meta.env.API_URL}/accounts/login`, {
method: "POST", method: "POST",
@ -27,24 +41,45 @@ if (!username || !password) {
}); });
if (!res.ok) { if (!res.ok) {
if (res.status === 401) { return {
error = "Identifiants invalides"; error:
} else { res.status === 401
error = "Une erreur est survenue"; ? "INVALID_CREDENTIALS"
: res.status === 404
? "NOT_FOUND"
: "SERVER_ERROR",
};
} }
} else {
const data = await res.json(); const data = (await res.json()) as LoginResponse;
Astro.cookies.set("jwt", data.jwt, { return { jwt: data.jwt };
} catch (err) {
return { error: "SERVER_ERROR" };
}
}
const req = Astro.request;
let error: string | undefined;
if (req.method === "POST") {
const form = await req.formData();
const username = form.get("username")?.toString().trim() ?? "";
const password = form.get("password")?.toString().trim() ?? "";
const { error: loginError, jwt } = await handleLogin(username, password);
if (loginError) {
error = ERROR_MESSAGES[loginError];
} else if (jwt) {
Astro.cookies.set("jwt", jwt, {
path: "/", path: "/",
secure: true, secure: true,
httpOnly: true, httpOnly: true,
sameSite: "strict",
}); });
return Astro.redirect("/"); return Astro.redirect("/");
} }
} catch (err) {
error = "Une erreur est survenue";
}
} }
--- ---

View file

@ -0,0 +1,15 @@
---
interface Props {
type?: "error" | "warning" | "success";
class?: string;
}
const { type = "error", class: className = "" } = Astro.props;
const typeClass = `message-${type}`;
---
<div class:list={["message", typeClass, className]}>
<slot />
</div>

View file

@ -0,0 +1,23 @@
---
interface Props {
type?: "button" | "submit" | "reset";
variant?: "default" | "primary" | "danger" | "warning";
class?: string;
fullWidth?: boolean;
}
const {
type = "button",
variant = "default",
class: className = "",
fullWidth = false,
} = Astro.props;
const variantClass = variant !== "default" ? `btn-${variant}` : "";
const widthClass = fullWidth ? "w-100" : "";
---
<button type={type} class:list={["btn", variantClass, widthClass, className]}>
<slot />
</button>

View file

@ -0,0 +1,30 @@
---
interface Props {
class?: string;
padding?: "sm" | "md" | "lg";
}
const { class: className = "", padding = "md" } = Astro.props;
const paddingMap = {
sm: "1rem",
md: "1.5rem",
lg: "2rem",
};
---
<div class:list={["card", className]} style={`padding: ${paddingMap[padding]}`}>
<slot />
</div>
<style>
.card :global(h2) {
background: var(--primary-gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
</style>

View file

@ -0,0 +1,68 @@
---
interface Props {
label?: string;
type?: "text" | "password" | "email" | "number";
name: string;
id?: string;
placeholder?: string;
value?: string | number;
required?: boolean;
error?: string;
class?: string;
}
const {
label,
type = "text",
name,
id = name,
placeholder = "",
value = "",
required = false,
error,
class: className = "",
} = Astro.props;
---
<div class:list={["form-group", className]}>
{
label && (
<label class="form-label" for={id}>
{label}
{required && <span class="required">*</span>}
</label>
)
}
<input
type={type}
id={id}
name={name}
class:list={["input", { "input-error": error }]}
placeholder={placeholder}
value={value}
required={required}
/>
{error && <div class="input-error-message">{error}</div>}
</div>
<style>
.required {
color: var(--error-color);
margin-left: 0.25rem;
}
.input-error {
border-color: var(--error-color);
}
.input-error:focus {
border-color: var(--error-color);
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.2);
}
.input-error-message {
color: var(--error-color);
font-size: 0.875rem;
margin-top: 0.5rem;
}
</style>

View file

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

View file

@ -11,8 +11,14 @@ interface Props {
const { title } = Astro.props; const { title } = Astro.props;
import "bootstrap/dist/css/bootstrap.min.css"; // Import des styles
import "../styles/index.css"; import "../styles/index.css";
import "../styles/grid.css";
import "../styles/components/buttons.css";
import "../styles/components/cards.css";
import "../styles/components/forms.css";
import "../styles/components/alerts.css";
import "../styles/utils.css";
--- ---
<!doctype html> <!doctype html>
@ -39,7 +45,3 @@ import "../styles/index.css";
height: 100%; height: 100%;
} }
</style> </style>
<script>
import "bootstrap/dist/js/bootstrap";
</script>

View file

@ -1,8 +1,23 @@
--- ---
import LoginFormData from "../components/forms/login/LoginFormData.astro"; import LoginFormData from "../components/forms/login/LoginFormData.astro";
import EmptyLayout from "../layouts/EmptyLayout.astro"; import EmptyLayout from "../layouts/EmptyLayout.astro";
import Card from "../components/ui/Card.astro";
--- ---
<EmptyLayout title="Trepa - Connexion"> <EmptyLayout title="Connexion">
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-12 col-md-6 col-lg-4">
<Card>
<LoginFormData /> <LoginFormData />
</Card>
</div>
</div>
</div>
</EmptyLayout> </EmptyLayout>
<style>
.container {
padding: 2rem;
}
</style>

View file

@ -0,0 +1,24 @@
/* Styles des messages d'alerte */
.message {
padding: 1rem;
border-radius: var(--radius-md);
margin-bottom: 1rem;
}
.message-error {
background: rgba(255, 107, 107, 0.1);
border: 1px solid var(--error-color);
color: var(--error-color);
}
.message-warning {
background: rgba(255, 217, 61, 0.1);
border: 1px solid var(--warning-color);
color: var(--warning-color);
}
.message-success {
background: rgba(107, 203, 119, 0.1);
border: 1px solid var(--success-color);
color: var(--success-color);
}

View file

@ -0,0 +1,33 @@
/* Styles des boutons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--radius-md);
font-weight: 500;
cursor: pointer;
transition: all var(--transition-speed);
background: var(--background-card);
color: var(--text-secondary);
backdrop-filter: var(--glass-blur);
}
.btn:hover {
opacity: 0.75;
}
.btn-primary {
background: var(--primary-gradient);
color: var(--background-dark);
font-weight: 600;
}
.btn-danger {
background: var(--error-color);
color: var(--text-primary);
}
.btn-warning {
background: var(--warning-color);
color: var(--background-dark);
font-weight: 600;
}

View file

@ -0,0 +1,8 @@
/* Styles des cartes */
.card {
background: var(--background-card);
border-radius: var(--radius-lg);
padding: 1.5rem;
backdrop-filter: var(--glass-blur);
border: 1px solid rgba(255, 255, 255, 0.1);
}

View file

@ -0,0 +1,27 @@
/* Styles des formulaires */
.input {
padding: 0.75rem 1rem;
background: var(--background-card);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius-md);
color: var(--text-primary);
backdrop-filter: var(--glass-blur);
width: 100%;
transition: all var(--transition-speed);
}
.input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(134, 227, 206, 0.2);
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}

260
src/styles/grid.css Normal file
View file

@ -0,0 +1,260 @@
/* Système de grille */
.container {
width: 100%;
margin-right: auto;
margin-left: auto;
padding-right: var(--container-padding);
padding-left: var(--container-padding);
}
@media (min-width: 640px) {
.container {
max-width: var(--breakpoint-sm);
}
}
@media (min-width: 768px) {
.container {
max-width: var(--breakpoint-md);
}
}
@media (min-width: 1024px) {
.container {
max-width: var(--breakpoint-lg);
}
}
@media (min-width: 1280px) {
.container {
max-width: var(--breakpoint-xl);
}
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: calc(-1 * var(--grid-gutter));
margin-left: calc(-1 * var(--grid-gutter));
}
[class^="col-"] {
position: relative;
width: 100%;
padding-right: var(--grid-gutter);
padding-left: var(--grid-gutter);
}
/* Colonnes */
.col {
flex: 1 0 0%;
}
.col-auto {
flex: 0 0 auto;
width: auto;
}
/* Small (sm) */
@media (min-width: 640px) {
.col-sm-1 {
flex: 0 0 var(--col-1);
max-width: var(--col-1);
}
.col-sm-2 {
flex: 0 0 var(--col-2);
max-width: var(--col-2);
}
.col-sm-3 {
flex: 0 0 var(--col-3);
max-width: var(--col-3);
}
.col-sm-4 {
flex: 0 0 var(--col-4);
max-width: var(--col-4);
}
.col-sm-5 {
flex: 0 0 var(--col-5);
max-width: var(--col-5);
}
.col-sm-6 {
flex: 0 0 var(--col-6);
max-width: var(--col-6);
}
.col-sm-7 {
flex: 0 0 var(--col-7);
max-width: var(--col-7);
}
.col-sm-8 {
flex: 0 0 var(--col-8);
max-width: var(--col-8);
}
.col-sm-9 {
flex: 0 0 var(--col-9);
max-width: var(--col-9);
}
.col-sm-10 {
flex: 0 0 var(--col-10);
max-width: var(--col-10);
}
.col-sm-11 {
flex: 0 0 var(--col-11);
max-width: var(--col-11);
}
.col-sm-12 {
flex: 0 0 var(--col-12);
max-width: var(--col-12);
}
}
/* Medium (md) */
@media (min-width: 768px) {
.col-md-1 {
flex: 0 0 var(--col-1);
max-width: var(--col-1);
}
.col-md-2 {
flex: 0 0 var(--col-2);
max-width: var(--col-2);
}
.col-md-3 {
flex: 0 0 var(--col-3);
max-width: var(--col-3);
}
.col-md-4 {
flex: 0 0 var(--col-4);
max-width: var(--col-4);
}
.col-md-5 {
flex: 0 0 var(--col-5);
max-width: var(--col-5);
}
.col-md-6 {
flex: 0 0 var(--col-6);
max-width: var(--col-6);
}
.col-md-7 {
flex: 0 0 var(--col-7);
max-width: var(--col-7);
}
.col-md-8 {
flex: 0 0 var(--col-8);
max-width: var(--col-8);
}
.col-md-9 {
flex: 0 0 var(--col-9);
max-width: var(--col-9);
}
.col-md-10 {
flex: 0 0 var(--col-10);
max-width: var(--col-10);
}
.col-md-11 {
flex: 0 0 var(--col-11);
max-width: var(--col-11);
}
.col-md-12 {
flex: 0 0 var(--col-12);
max-width: var(--col-12);
}
}
/* Large (lg) */
@media (min-width: 1024px) {
.col-lg-1 {
flex: 0 0 var(--col-1);
max-width: var(--col-1);
}
.col-lg-2 {
flex: 0 0 var(--col-2);
max-width: var(--col-2);
}
.col-lg-3 {
flex: 0 0 var(--col-3);
max-width: var(--col-3);
}
.col-lg-4 {
flex: 0 0 var(--col-4);
max-width: var(--col-4);
}
.col-lg-5 {
flex: 0 0 var(--col-5);
max-width: var(--col-5);
}
.col-lg-6 {
flex: 0 0 var(--col-6);
max-width: var(--col-6);
}
.col-lg-7 {
flex: 0 0 var(--col-7);
max-width: var(--col-7);
}
.col-lg-8 {
flex: 0 0 var(--col-8);
max-width: var(--col-8);
}
.col-lg-9 {
flex: 0 0 var(--col-9);
max-width: var(--col-9);
}
.col-lg-10 {
flex: 0 0 var(--col-10);
max-width: var(--col-10);
}
.col-lg-11 {
flex: 0 0 var(--col-11);
max-width: var(--col-11);
}
.col-lg-12 {
flex: 0 0 var(--col-12);
max-width: var(--col-12);
}
}
/* Extra Large (xl) */
@media (min-width: 1280px) {
.col-xl-1 {
flex: 0 0 var(--col-1);
max-width: var(--col-1);
}
.col-xl-2 {
flex: 0 0 var(--col-2);
max-width: var(--col-2);
}
.col-xl-3 {
flex: 0 0 var(--col-3);
max-width: var(--col-3);
}
.col-xl-4 {
flex: 0 0 var(--col-4);
max-width: var(--col-4);
}
.col-xl-5 {
flex: 0 0 var(--col-5);
max-width: var(--col-5);
}
.col-xl-6 {
flex: 0 0 var(--col-6);
max-width: var(--col-6);
}
.col-xl-7 {
flex: 0 0 var(--col-7);
max-width: var(--col-7);
}
.col-xl-8 {
flex: 0 0 var(--col-8);
max-width: var(--col-8);
}
.col-xl-9 {
flex: 0 0 var(--col-9);
max-width: var(--col-9);
}
.col-xl-10 {
flex: 0 0 var(--col-10);
max-width: var(--col-10);
}
.col-xl-11 {
flex: 0 0 var(--col-11);
max-width: var(--col-11);
}
.col-xl-12 {
flex: 0 0 var(--col-12);
max-width: var(--col-12);
}
}

View file

@ -1,27 +1,62 @@
.cardshadow { /* Variables globales */
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4); :root {
transition: box-shadow 0.2s ease-in-out; /* Couleurs principales */
--primary-gradient: linear-gradient(135deg, #86e3ce 0%, #d6a4e5 100%);
--background-dark: #1a1b1e;
--background-card: rgba(255, 255, 255, 0.05);
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.7);
--accent-color: #86e3ce;
--error-color: #ff6b6b;
--warning-color: #ffd93d;
--success-color: #6bcb77;
/* Effets */
--glass-blur: blur(10px);
--card-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
--transition-speed: 0.3s;
/* Border radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
/* Grid */
--grid-gutter: 1rem;
--container-padding: 1rem;
/* Breakpoints */
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
/* Column widths */
--col-1: 8.333333%;
--col-2: 16.666667%;
--col-3: 25%;
--col-4: 33.333333%;
--col-5: 41.666667%;
--col-6: 50%;
--col-7: 58.333333%;
--col-8: 66.666667%;
--col-9: 75%;
--col-10: 83.333333%;
--col-11: 91.666667%;
--col-12: 100%;
} }
.cardshadow:hover { /* Reset et styles de base */
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.8); * {
margin: 0;
padding: 0;
box-sizing: border-box;
} }
.movieitem { body {
opacity: 0; background-color: var(--background-dark);
background-color: rgba(0, 0, 0, 0.75); color: var(--text-primary);
color: rgba(255, 255, 255, 1); font-family: "Inter", system-ui, sans-serif;
} line-height: 1.5;
min-height: 100vh;
.movieitem:hover {
animation: 0.2s ease-in-out forwards moviehover;
}
@keyframes moviehover {
from {
opacity: 0;
}
to {
opacity: 1;
}
} }

89
src/styles/utils.css Normal file
View file

@ -0,0 +1,89 @@
/* Classes utilitaires */
/* Flex */
.d-flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.justify-content-center {
justify-content: center;
}
.align-items-center {
align-items: center;
}
/* Text */
.text-center {
text-align: center;
}
/* Dimensions */
.w-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
/* Marges */
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-1 {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.my-3 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.my-4 {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.my-5 {
margin-top: 2rem;
margin-bottom: 2rem;
}
/* Séparateur */
hr {
border: none;
height: 1px;
margin: 2rem 0;
background: linear-gradient(
90deg,
transparent,
var(--text-secondary),
transparent
);
opacity: 0.2;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fadeIn 0.5s ease-out;
}