diff --git a/bun.lockb b/bun.lockb index 2f4f837..5d2027e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index fb2cb2c..dc25952 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "astro": "^5.5.5", - "bootstrap": "^5.3.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.5.0", diff --git a/src/components/Logo.astro b/src/components/Logo.astro new file mode 100644 index 0000000..81e839c --- /dev/null +++ b/src/components/Logo.astro @@ -0,0 +1,56 @@ +--- +interface Props { + class?: string; +} + +const { class: className = "" } = Astro.props; +--- + + + + diff --git a/src/components/forms/login/LoginForm.astro b/src/components/forms/login/LoginForm.astro index 4b7b8eb..02060b5 100644 --- a/src/components/forms/login/LoginForm.astro +++ b/src/components/forms/login/LoginForm.astro @@ -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 { - error: string | null; + error?: string; + username?: string; + password?: string; } -const { error } = Astro.props as Props; +const { error, username = "", password = "" } = Astro.props; --- -
-
- { - error && ( -
- {error} -
- ) - } -
-
- - -
-
- - -
-
- -
-
+ +
+{ + error && ( + + {error} + + ) +} +
+
+ +
-
+
+ + +
+ + + + diff --git a/src/components/forms/login/LoginFormData.astro b/src/components/forms/login/LoginFormData.astro index 7a41a46..54ddb49 100644 --- a/src/components/forms/login/LoginFormData.astro +++ b/src/components/forms/login/LoginFormData.astro @@ -1,22 +1,36 @@ --- import LoginForm from "./LoginForm.astro"; -const req = Astro.request; -const isPost = req.method === "POST"; +type LoginError = + | "MISSING_FIELDS" + | "INVALID_CREDENTIALS" + | "SERVER_ERROR" + | "NOT_FOUND"; -let username = ""; -let password = ""; -let error = null; +const ERROR_MESSAGES: Record = { + MISSING_FIELDS: "Tous les champs sont requis", + INVALID_CREDENTIALS: "Identifiants invalides", + NOT_FOUND: "Compte non trouvé", + SERVER_ERROR: "Une erreur est survenue", +}; -if (isPost) { - const form = await req.formData(); - username = form.get("username")?.toString().trim() ?? ""; - password = form.get("password")?.toString().trim() ?? ""; +interface LoginResponse { + jwt: string; } -if (!username || !password) { - error = "Tous les champs sont requis"; -} else { +interface LoginResult { + error?: LoginError; + jwt?: string; +} + +async function handleLogin( + username: string, + password: string, +): Promise { + if (!username || !password) { + return { error: "MISSING_FIELDS" }; + } + try { const res = await fetch(`${import.meta.env.API_URL}/accounts/login`, { method: "POST", @@ -27,23 +41,44 @@ if (!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("/"); + return { + error: + res.status === 401 + ? "INVALID_CREDENTIALS" + : res.status === 404 + ? "NOT_FOUND" + : "SERVER_ERROR", + }; } + + const data = (await res.json()) as LoginResponse; + return { jwt: data.jwt }; } catch (err) { - error = "Une erreur est survenue"; + 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: "/", + secure: true, + httpOnly: true, + sameSite: "strict", + }); + + return Astro.redirect("/"); } } --- diff --git a/src/components/ui/Alert.astro b/src/components/ui/Alert.astro new file mode 100644 index 0000000..56c37a8 --- /dev/null +++ b/src/components/ui/Alert.astro @@ -0,0 +1,15 @@ +--- +interface Props { + type?: "error" | "warning" | "success"; + class?: string; +} + +const { type = "error", class: className = "" } = Astro.props; + +const typeClass = `message-${type}`; +--- + +
+ +
+ diff --git a/src/components/ui/Button.astro b/src/components/ui/Button.astro new file mode 100644 index 0000000..9464004 --- /dev/null +++ b/src/components/ui/Button.astro @@ -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" : ""; +--- + + + diff --git a/src/components/ui/Card.astro b/src/components/ui/Card.astro new file mode 100644 index 0000000..b994c18 --- /dev/null +++ b/src/components/ui/Card.astro @@ -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", +}; +--- + +
+ +
+ + + diff --git a/src/components/ui/Input.astro b/src/components/ui/Input.astro new file mode 100644 index 0000000..cdfc125 --- /dev/null +++ b/src/components/ui/Input.astro @@ -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; +--- + +
+ { + label && ( + + ) + } + + {error &&
{error}
} +
+ + diff --git a/src/layouts/EmptyLayout.astro b/src/layouts/EmptyLayout.astro index 286079e..f1fe919 100644 --- a/src/layouts/EmptyLayout.astro +++ b/src/layouts/EmptyLayout.astro @@ -9,14 +9,17 @@ const { title } = Astro.props; --- -
-
-
- -
-
+
+
+ + diff --git a/src/layouts/RootLayout.astro b/src/layouts/RootLayout.astro index 4a08673..b98f4d4 100644 --- a/src/layouts/RootLayout.astro +++ b/src/layouts/RootLayout.astro @@ -11,8 +11,14 @@ interface Props { const { title } = Astro.props; -import "bootstrap/dist/css/bootstrap.min.css"; +// Import des styles 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"; --- @@ -39,7 +45,3 @@ import "../styles/index.css"; height: 100%; } - - diff --git a/src/pages/login.astro b/src/pages/login.astro index 1362bea..d5e0c2b 100644 --- a/src/pages/login.astro +++ b/src/pages/login.astro @@ -1,8 +1,23 @@ --- import LoginFormData from "../components/forms/login/LoginFormData.astro"; import EmptyLayout from "../layouts/EmptyLayout.astro"; +import Card from "../components/ui/Card.astro"; --- - - + +
+
+
+ + + +
+
+
+ + diff --git a/src/styles/components/alerts.css b/src/styles/components/alerts.css new file mode 100644 index 0000000..bab4e08 --- /dev/null +++ b/src/styles/components/alerts.css @@ -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); +} diff --git a/src/styles/components/buttons.css b/src/styles/components/buttons.css new file mode 100644 index 0000000..d89c8fe --- /dev/null +++ b/src/styles/components/buttons.css @@ -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; +} diff --git a/src/styles/components/cards.css b/src/styles/components/cards.css new file mode 100644 index 0000000..696eece --- /dev/null +++ b/src/styles/components/cards.css @@ -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); +} diff --git a/src/styles/components/forms.css b/src/styles/components/forms.css new file mode 100644 index 0000000..ee06ead --- /dev/null +++ b/src/styles/components/forms.css @@ -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); +} diff --git a/src/styles/grid.css b/src/styles/grid.css new file mode 100644 index 0000000..706e45a --- /dev/null +++ b/src/styles/grid.css @@ -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); + } +} diff --git a/src/styles/index.css b/src/styles/index.css index 217912a..dd54de2 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,27 +1,62 @@ -.cardshadow { - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4); - transition: box-shadow 0.2s ease-in-out; +/* Variables globales */ +:root { + /* 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 { - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.8); +/* Reset et styles de base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; } -.movieitem { - opacity: 0; - background-color: rgba(0, 0, 0, 0.75); - color: rgba(255, 255, 255, 1); -} - -.movieitem:hover { - animation: 0.2s ease-in-out forwards moviehover; -} - -@keyframes moviehover { - from { - opacity: 0; - } - to { - opacity: 1; - } +body { + background-color: var(--background-dark); + color: var(--text-primary); + font-family: "Inter", system-ui, sans-serif; + line-height: 1.5; + min-height: 100vh; } diff --git a/src/styles/utils.css b/src/styles/utils.css new file mode 100644 index 0000000..fea2f90 --- /dev/null +++ b/src/styles/utils.css @@ -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; +}