style: login page Okay

This commit is contained in:
Ruben 2025-04-01 19:26:46 +01:00
parent 921007ae13
commit 8c12667693
12 changed files with 367 additions and 173 deletions

View File

@ -14,8 +14,11 @@
"@tanstack/react-query": "^5.69.0",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.4",
"bootstrap": "^5.3.3",
"clsx": "^2.1.1",
"declarations": "link:@/lib/declarations",
"jwt-decode": "^4.0.0",
"leaflet": "^1.9.4",
"next": "15.2.3",
"next-auth": "^4.24.11",
"nextjs-toploader": "^3.8.15",
@ -32,6 +35,7 @@
"@eslint/eslintrc": "^3",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/postcss": "^4",
"@types/leaflet": "^1.9.17",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@ -39,7 +43,6 @@
"eslint-config-next": "15.2.3",
"install": "^0.13.0",
"npm": "^11.2.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}

46
pnpm-lock.yaml generated
View File

@ -23,12 +23,21 @@ importers:
axios:
specifier: ^1.8.4
version: 1.8.4
bootstrap:
specifier: ^5.3.3
version: 5.3.3(@popperjs/core@2.11.8)
clsx:
specifier: ^2.1.1
version: 2.1.1
declarations:
specifier: link:@/lib/declarations
version: link:@/lib/declarations
jwt-decode:
specifier: ^4.0.0
version: 4.0.0
leaflet:
specifier: ^1.9.4
version: 1.9.4
next:
specifier: 15.2.3
version: 15.2.3(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.0)
@ -72,6 +81,9 @@ importers:
'@tailwindcss/postcss':
specifier: ^4
version: 4.0.15
'@types/leaflet':
specifier: ^1.9.17
version: 1.9.17
'@types/node':
specifier: ^20
version: 20.17.27
@ -93,9 +105,6 @@ importers:
npm:
specifier: ^11.2.0
version: 11.2.0
tailwindcss:
specifier: ^4
version: 4.0.15
typescript:
specifier: ^5
version: 5.8.2
@ -1018,6 +1027,9 @@ packages:
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
engines: {node: '>= 10.0.0'}
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@radix-ui/colors@3.0.0':
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
@ -1875,12 +1887,18 @@ packages:
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/leaflet@1.9.17':
resolution: {integrity: sha512-IJ4K6t7I3Fh5qXbQ1uwL3CFVbCi6haW9+53oLWgdKlLP7EaS21byWFJxxqOx9y8I0AP0actXSJLVMbyvxhkUTA==}
'@types/node@20.17.27':
resolution: {integrity: sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==}
@ -2100,6 +2118,11 @@ packages:
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
bootstrap@5.3.3:
resolution: {integrity: sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==}
peerDependencies:
'@popperjs/core': ^2.11.8
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@ -2845,6 +2868,9 @@ packages:
resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
engines: {node: '>=0.10'}
leaflet@1.9.4:
resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@ -4716,6 +4742,8 @@ snapshots:
'@parcel/watcher-win32-x64': 2.5.1
optional: true
'@popperjs/core@2.11.8': {}
'@radix-ui/colors@3.0.0': {}
'@radix-ui/number@1.1.0': {}
@ -5614,10 +5642,16 @@ snapshots:
'@types/estree@1.0.7': {}
'@types/geojson@7946.0.16': {}
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}
'@types/leaflet@1.9.17':
dependencies:
'@types/geojson': 7946.0.16
'@types/node@20.17.27':
dependencies:
undici-types: 6.19.8
@ -5882,6 +5916,10 @@ snapshots:
boolbase@1.0.0: {}
bootstrap@5.3.3(@popperjs/core@2.11.8):
dependencies:
'@popperjs/core': 2.11.8
brace-expansion@1.1.11:
dependencies:
balanced-match: 1.0.2
@ -6765,6 +6803,8 @@ snapshots:
dependencies:
language-subtag-registry: 0.3.23
leaflet@1.9.4: {}
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1

View File

@ -7,71 +7,77 @@ import { signIn } from "next-auth/react"
import { useRouter, useSearchParams } from "next/navigation";
export default function LoginPage() {
const router = useRouter()
const params = useSearchParams().get("redirect_to");
const router = useRouter()
const params = useSearchParams().get("redirect_to");
const mutation = useMutation({
mutationKey: ['login'],
mutationFn: async (data: { email: string; password: string }) => {
try {
const result = await signIn("credentials", {
email: data.email,
password: data.password,
redirect: false,
})
if (result?.error) {
const errorMessage = result.error.includes("CredentialsSignin")
? "Email ou mot de passe incorrect"
: result.error;
console.error(errorMessage)
throw new Error(result.error)
} else {
if (params) {
router.push(params);
} else {
router.push('/admin/home')
}
}
return result
} catch (error: unknown) {
if (error instanceof Error && error.message.includes("Network Error")) {
console.error("Problème de connexion au serveur");
}
console.error("Autre = ", error);
}
const mutation = useMutation({
mutationKey: ['login'],
mutationFn: async (data: { email: string; password: string }) => {
try {
const result = await signIn("credentials", {
email: data.email,
password: data.password,
redirect: false,
})
},
onError: (error: Error) => {
console.error(error.message)
},
})
if (result?.error) {
const errorMessage = result.error.includes("CredentialsSignin")
? "Email ou mot de passe incorrect"
: result.error;
console.error(errorMessage)
throw new Error(result.error)
} else {
if (params) {
router.push(params);
} else {
router.push('/admin/home')
}
}
return result
} catch (error: unknown) {
if (error instanceof Error && error.message.includes("Network Error")) {
console.error("Problème de connexion au serveur");
}
console.error("Autre = ", error);
}
return(
<div>
<Form
title="Connexion"
formClassName="bg-white p-10 shadow-2xl w-3/4 lg:w-lg"
fields={[
{
label: "Email",
name: "email",
type: "email",
placeholder: "Entrer votre email"
},
{
label: "Password",
name: "password",
type: "password",
placeholder: "Enter votre mot de passe",
showPasswordToggle: true
}
]}
submit={mutation.mutate}
schema={loginSchema}
child={<button disabled={mutation.isPending} type="submit" className={`${mutation.isPending ? "btn-auth-loading" : "btn-auth"} mt-4`}>{mutation.isPending ? "Chargement..." : "Connexion"}</button>}
/>
</div>
)
},
onError: (error: Error) => {
console.error(error.message)
},
})
return (
<div className="d-flex flex-column bg-bluegray h-100 align-items-center">
<div className="login-main d-flex justify-content-center align-items-center position-relative">
<Form
title="Connexion"
formClassName="bg-white p-10 shadow-2xl w-3/4 lg:w-lg"
fields={[
{
label: "Email",
name: "email",
type: "email",
placeholder: "Entrer votre email"
},
{
label: "Password",
name: "password",
type: "password",
placeholder: "Enter votre mot de passe",
showPasswordToggle: true
}
]}
submit={mutation.mutate}
schema={loginSchema}
child={<div className="d-flex justify-content-center w-100"> <button disabled={mutation.isPending} type="submit" className={`${mutation.isPending ? "btn-auth-loading" : "btn-auth"} cta modal-cta`}>{mutation.isPending ? "Chargement..." : "Connexion"}</button></div>}
/>
</div>
<div className="version">
<p className="fw-bold" >Version 0.0.1</p>
</div>
</div>
)
}

View File

@ -1,8 +1,6 @@
@import "tailwindcss";
@font-face {
font-family: 'Urbanist'; /* Nom que vous donnerez à votre police */
src: url('../assets/fonts/Urbanist.ttf') format('truetype'); /* Chemin vers votre fichier */
font-family: 'Urbanist';
src: url('../assets/fonts/Urbanist.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@ -29,15 +27,18 @@
--background: #ffffff;
--cinder: #999;
--bluegray: --primary;
}
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Urbanist, sans-serif;
font-family: Urbanist, sans-serif !important;
}
*{
margin: 0;
padding: 0;
}
.input-form {
width: 100%;
padding: 12px;
@ -54,14 +55,22 @@ body {
position: absolute;
left: 12px;
top: -0.45rem;
background-color: white;
padding-inline: 4px;
padding: 1px 5px;
line-height: normal;
color: var(--secondary);
background-color: var(--background);
border-radius: 400px;
z-index: 1;
}
.form-error {
font-size: 14px;
color: var(--danger);
}
.btn-floating-right {
position: absolute;
right: 8px;
right: 12px;
top: 50%;
transform: translateY(-50%);
}
@ -102,7 +111,12 @@ body {
transition: width 0.2s, height 0.2s;
}
.cta{
button {
background-color: transparent;
border: none;
}
.cta {
padding: 10px 24px;
width: max-content;
height: max-content;
@ -116,35 +130,38 @@ body {
text-wrap: nowrap;
}
.cta.modal-cta{
.cta.modal-cta {
padding: 9px;
margin-top: 32px;
width: 240px;
height: 40px;
}
.cta.cancel{
.cta.cancel {
color: var(--secondary);
border: 1px solid var(--gray);
background-color: var(--gray);
}
.cta.info{
.cta.info {
color: var(--primary);
background-color: var(--background);
}
.cta.danger{
.cta.danger {
border: 1px solid var(--danger);
background-color: var(--danger);
}
.bg-bluegray{
.bg-bluegray {
background-color: var(--bluegray);
}
.bg-gray{
.bg-gray {
background-color: var(--gray);
}
hr{
hr {
color: var(--gray);
}
@ -155,10 +172,10 @@ input[type="checkbox"] {
width: 24px;
height: 24px;
border: 1px solid var(--secondary) !important;
border-radius: 6px !important;
background-color: transparent;
cursor: pointer;
border: 1px solid var(--secondary) !important;
border-radius: 6px !important;
background-color: transparent;
cursor: pointer;
position: relative;
}
@ -170,7 +187,7 @@ input[type="checkbox"]:checked {
input[type="checkbox"]:checked::before {
content: "✔";
content: "✔";
font-size: 18px;
color: white;
position: absolute;
@ -179,7 +196,7 @@ input[type="checkbox"]:checked::before {
transform: translate(-50%, -50%);
}
.modal-input{
.modal-input {
width: 490px;
margin-left: auto;
margin-right: auto;
@ -189,7 +206,7 @@ input[type="checkbox"]:checked::before {
/* Scroll Bar */
::-webkit-scrollbar {
width: 4px;
width: 7px;
padding: 0;
margin: 0;
}
@ -224,4 +241,95 @@ input[type="checkbox"]:checked::before {
width: 16px;
height: 16px;
}
}
/* Login Header */
.login-header {
padding: 20px 0;
box-shadow: 0 0 24px #0000001A;
}
.word {
font-size: 24px;
font-weight: bold;
color: black;
position: relative;
line-height: normal;
}
.dot {
/* font-size: 14px; */
color: blue;
position: absolute;
top: -10px;
left: 22px;
animation: bounce 1s infinite ease-in-out;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-3px);
}
}
.login-main {
min-height: 100vh;
}
.form-container {
padding: 48px 64px;
margin: auto;
width: max-content;
max-width: 624px;
border-radius: 8px;
box-shadow: 0 0 24px #0000001A;
}
.form-fields {
gap: 24px;
}
@media (max-width: 768px) {
.login-main{
width: 100%;
}
.form-container {
padding: 24px 32px;
margin: 0 4%;
width: 80%;
}
.form-fields {
gap: 12px;
}
.modal-input {
width: 100%;
min-width: 100%;
}
.form-title{
margin-bottom: 2px!important;
}
.cta.modal-cta{
margin: 0!important;
}
.input-form{
margin: 0 auto;
}
}

View File

@ -21,6 +21,8 @@ export default function RootLayout({
<head>
<link rel="icon" href="/favicon.svg" />
<link rel="favicon.svg" href="/favicon.svg" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossOrigin="anonymous"/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossOrigin="anonymous"></script>
</head>
<body>
<AuthProvider>

View File

@ -0,0 +1,16 @@
import React from "react";
//import { GeneralHtmlAttr } from "@/lib/declarations";
//export type GeneralHtmlAttr = React.HTMLAttributes<HTMLDivElement>;
import L from "leaflet";
export const HomeIcon = (props) => {
return (
<div {...props}>
<svg width="22" height="21" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.25 11L10.205 2.045C10.3094 1.94056 10.4333 1.85772 10.5697 1.80119C10.7061 1.74467 10.8523 1.71558 11 1.71558C11.1477 1.71558 11.2939 1.74467 11.4303 1.80119C11.5667 1.85772 11.6906 1.94056 11.795 2.045L20.75 11M3.5 8.75V18.875C3.5 19.496 4.004 20 4.625 20H8.75V15.125C8.75 14.504 9.254 14 9.875 14H12.125C12.746 14 13.25 14.504 13.25 15.125V20H17.375C17.996 20 18.5 19.496 18.5 18.875V8.75M7.25 20H15.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 462 KiB

View File

@ -30,7 +30,7 @@ export default function FloatingLabelInput({
switch(type) {
case 'select':
return (
<div className="relative w-full">
<div className="position-relative w-full">
<select
className="input-form modal-input focus:ring-2 focus:ring-blue-500 outline-none"
name={name}
@ -46,7 +46,7 @@ export default function FloatingLabelInput({
case 'password':
return (
<div className="relative w-full">
<div className="position-relative w-full">
<input
name={name}
type={showPassword ? "text" : "password"}
@ -66,14 +66,14 @@ export default function FloatingLabelInput({
src={icons.eyeSlashIcon}
width={20}
height={20}
alt=''
alt='show password'
/>
) : (
<Image
src={icons.eyeIcon}
width={20}
height={20}
alt=''
alt='hide password'
/>
)}
</button>
@ -84,7 +84,7 @@ export default function FloatingLabelInput({
case 'search':
return (
<div className="relative w-full">
<div className="position-relative w-full">
<div className='btn-floating-left'>
<Image alt='' src={icons.searchIcon} />
</div>
@ -102,7 +102,7 @@ export default function FloatingLabelInput({
default:
return (
<div className="relative w-full">
<div className="position-relative w-full">
<input
type={type}
placeholder={placeholder}
@ -118,7 +118,7 @@ export default function FloatingLabelInput({
};
return (
<div className="relative">
<div className="position-relative">
<label
htmlFor={name}
className="input-label text-gray-400 text-sm"

View File

@ -5,72 +5,71 @@ import { FormProps } from "#/types"
import { FormEvent, useState } from "react"
export default function Form({
fields,
submit,
className,
child,
title,
schema,
formClassName
} : FormProps) {
const [errors, setErrors] = useState<Record<string, string>>({});
fields,
submit,
className,
child,
title,
schema,
formClassName
}: FormProps) {
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const data = Object.fromEntries(formData)
const formData = new FormData(e.currentTarget)
const data = Object.fromEntries(formData)
console.log("FORM DATA = ", data)
const result = schema.safeParse(data);
console.log("ZOD = ", result.error?.format())
console.log("FORM DATA = ", data)
const result = schema.safeParse(data);
console.log("ZOD = ", result.error?.format())
if(!result.success) {
const formatedErrors = result.error.format() as Record<string, { _errors?: string[] }>;
if (!result.success) {
const formatedErrors = result.error.format() as Record<string, { _errors?: string[] }>;
const newErrors: Record<string, string> = {};
Object.keys(formatedErrors).forEach((field) => {
if (field !== "_errors" && formatedErrors[field]._errors?.length) {
newErrors[field] = formatedErrors[field]._errors[0];
}
});
setErrors(newErrors)
} else {
setErrors({})
submit(result.data)
const newErrors: Record<string, string> = {};
Object.keys(formatedErrors).forEach((field) => {
if (field !== "_errors" && formatedErrors[field]._errors?.length) {
newErrors[field] = formatedErrors[field]._errors[0];
}
});
setErrors(newErrors)
} else {
setErrors({})
submit(result.data)
}
}
return (
<form onSubmit={handleSubmit} className={formClassName}>
<div className="flex justify-center text-black">
<p className="text-3xl font-bold">{title}</p>
return (
<form onSubmit={handleSubmit} className={(formClassName) + " form-container"}>
<div className="form-title d-flex justify-content-center text-black mb-3 ">
<p className="fs-3 fw-bold">{title}</p>
</div>
<div className={`d-flex flex-column form-fields ${className}`}>
{
fields.map((item, index) => (
<div key={index}>
<FloatingLabelInput
label={item.label}
name={item.name}
type={item.type}
button={item.button}
defaultValue={item.defaultValue}
options={item.options}
placeholder={item.placeholder}
showPasswordToggle={item.showPasswordToggle}
/>
<span className="form-error">{errors[item.name]}</span>
</div>
<div className={`space-y-8 my-2 ${className}`}>
{
fields.map((item, index) => (
<div key={index}>
<FloatingLabelInput
label={item.label}
name={item.name}
type={item.type}
button={item.button}
defaultValue={item.defaultValue}
options={item.options}
placeholder={item.placeholder}
showPasswordToggle={item.showPasswordToggle}
/>
<span className="text-red-500 text-xs mt-1">{errors[item.name]}</span>
</div>
))
}
</div>
{child}
</form>
)
))
}
</div>
{child}
</form>
)
}

View File

@ -2,16 +2,18 @@ import { icons } from "#/assets/icons";
import Image from "next/image";
export default function Header() {
return(
<div className="w-full bg-white shadow-md py-4">
<div className="container mx-auto text-center flex items-center justify-center gap-2">
<Image
src={icons.logo}
alt="Private Docs"
className="text-red-500 h-auto"
/>
<p className="text-2xl font-bold text-black">Private Docs</p>
</div>
</div>
)
return (
<div className="d-flex justify-content-center login-header ">
<div className="d-flex justify-content-center align-items-center gap-2">
<Image
src={icons.logo}
alt="Private Docs"
className="text-red-500 h-auto"
height={30}
/>
{/* <p className="text-2xl font-bold text-black">Private Docs</p> */}
<p className="r-p-m-0 word">Pr<span className="dot"></span>ıvate Docs</p>
</div>
</div>
)
}

View File

@ -0,0 +1,9 @@
"use client";
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
export default function Layout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}

View File

@ -23,6 +23,6 @@
"@/*": ["./src/*"]
}
},
"include": ["svgr.d.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["svgr.d.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/assets/icons/final-index.js"],
"exclude": ["node_modules"]
}