Compare commits
8 Commits
99f3be65bc
...
98d68a535b
| Author | SHA1 | Date | |
|---|---|---|---|
| 98d68a535b | |||
| 130473d3e4 | |||
| 42b0e8b3a0 | |||
| e967d9d8ac | |||
| 44c19d2813 | |||
| ae7f66aa78 | |||
| b7e3b56739 | |||
| 09680eac63 |
12
auth.d.ts
vendored
Normal file
12
auth.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { DefaultSession } from "next-auth";
|
||||||
|
|
||||||
|
declare module "next-auth" {
|
||||||
|
export interface User extends Partial<DefaultSession<User>> {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.69.0",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"next": "15.2.3",
|
"next": "15.2.3",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
|
|||||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@ -11,9 +11,15 @@ importers:
|
|||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.69.0
|
specifier: ^5.69.0
|
||||||
version: 5.69.0(react@19.0.0)
|
version: 5.69.0(react@19.0.0)
|
||||||
|
'@tanstack/react-table':
|
||||||
|
specifier: ^8.21.2
|
||||||
|
version: 8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.8.4
|
specifier: ^1.8.4
|
||||||
version: 1.8.4
|
version: 1.8.4
|
||||||
|
clsx:
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
jwt-decode:
|
jwt-decode:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
@ -1153,6 +1159,17 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18 || ^19
|
react: ^18 || ^19
|
||||||
|
|
||||||
|
'@tanstack/react-table@8.21.2':
|
||||||
|
resolution: {integrity: sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8'
|
||||||
|
react-dom: '>=16.8'
|
||||||
|
|
||||||
|
'@tanstack/table-core@8.21.2':
|
||||||
|
resolution: {integrity: sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@trysound/sax@0.2.0':
|
'@trysound/sax@0.2.0':
|
||||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@ -1437,6 +1454,10 @@ packages:
|
|||||||
client-only@0.0.1:
|
client-only@0.0.1:
|
||||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||||
|
|
||||||
|
clsx@2.1.1:
|
||||||
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@ -3996,6 +4017,14 @@ snapshots:
|
|||||||
'@tanstack/query-core': 5.69.0
|
'@tanstack/query-core': 5.69.0
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
|
|
||||||
|
'@tanstack/react-table@8.21.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/table-core': 8.21.2
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
|
||||||
|
'@tanstack/table-core@8.21.2': {}
|
||||||
|
|
||||||
'@trysound/sax@0.2.0': {}
|
'@trysound/sax@0.2.0': {}
|
||||||
|
|
||||||
'@tybys/wasm-util@0.9.0':
|
'@tybys/wasm-util@0.9.0':
|
||||||
@ -4327,6 +4356,8 @@ snapshots:
|
|||||||
|
|
||||||
client-only@0.0.1: {}
|
client-only@0.0.1: {}
|
||||||
|
|
||||||
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Form from "#/components/form/form"
|
import Form from "#/components/form/form"
|
||||||
|
import { loginSchema } from "#/schema/loginSchema"
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return(
|
return(
|
||||||
@ -17,10 +20,12 @@ export default function LoginPage() {
|
|||||||
label: "Password",
|
label: "Password",
|
||||||
name: "password",
|
name: "password",
|
||||||
type: "password",
|
type: "password",
|
||||||
placeholder: "Enter votre mot de passe"
|
placeholder: "Enter votre mot de passe",
|
||||||
|
showPasswordToggle: true
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
submit={undefined}
|
submit={undefined}
|
||||||
|
schema={loginSchema}
|
||||||
child={<button type="submit" className="btn-auth">Connexion</button>}
|
child={<button type="submit" className="btn-auth">Connexion</button>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,10 +1,129 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import Table from "#/components/table/table"
|
||||||
|
import { ColumnDef } from "@tanstack/react-table"
|
||||||
|
|
||||||
|
|
||||||
export default function HomePage () {
|
export default function HomePage () {
|
||||||
|
type Payment = {
|
||||||
|
id: string
|
||||||
|
amount: number
|
||||||
|
status: "pending" | "processing" | "success" | "failed"
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const data: Payment[] = [
|
||||||
|
{
|
||||||
|
id: "728ed52f",
|
||||||
|
amount: 100,
|
||||||
|
status: "pending",
|
||||||
|
email: "m@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "728ed521",
|
||||||
|
amount: 200,
|
||||||
|
status: "pending",
|
||||||
|
email: "j@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "728ed528",
|
||||||
|
amount: 300,
|
||||||
|
status: "processing",
|
||||||
|
email: "f@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "728ed52g",
|
||||||
|
amount: 600,
|
||||||
|
status: "success",
|
||||||
|
email: "h@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "728ed520",
|
||||||
|
amount: 50,
|
||||||
|
status: "failed",
|
||||||
|
email: "k@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "728ed529",
|
||||||
|
amount: 200,
|
||||||
|
status: "pending",
|
||||||
|
email: "l@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "728ed526",
|
||||||
|
amount: 150,
|
||||||
|
status: "processing",
|
||||||
|
email: "d@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "728ed523",
|
||||||
|
amount: 100,
|
||||||
|
status: "success",
|
||||||
|
email: "o@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "728ed52y",
|
||||||
|
amount: 100,
|
||||||
|
status: "failed",
|
||||||
|
email: "v@example.com",
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const columns: ColumnDef<Payment>[] = [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<input checked={
|
||||||
|
table.getIsAllPageRowsSelected() ||
|
||||||
|
(table.getIsSomePageRowsSelected() && undefined)
|
||||||
|
} onChange={(value) => table.toggleAllPageRowsSelected(!!value)} type="checkbox" name="" id="" />
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<input checked={row.getIsSelected()} onChange={(value) => row.toggleSelected(!!value)} type="checkbox" name="" id="" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: "Status",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "email",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<p>Email</p>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "amount",
|
||||||
|
header: () => <div className="">Amount</div>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const amount = parseFloat(row.getValue("amount"))
|
||||||
|
const formatted = new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
}).format(amount)
|
||||||
|
|
||||||
|
return <div className="font-medium">{formatted}</div>
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<>
|
<>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
pageSize={5}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import NextAuth, { User } from "next-auth";
|
import NextAuth, { User } from "next-auth";
|
||||||
import Credentials from "next-auth/providers/credentials";
|
import Credentials from "next-auth/providers/credentials";
|
||||||
import axios, { AxiosError } from "axios";
|
import axios from "axios";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
const handler = NextAuth({
|
const handler = NextAuth({
|
||||||
@ -11,37 +11,58 @@ const handler = NextAuth({
|
|||||||
password: {},
|
password: {},
|
||||||
},
|
},
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
let user: User | null = null;
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
const response = axios({
|
'private-docs-api.intside.co/users/login/',
|
||||||
method: 'post',
|
{
|
||||||
url: 'private-docs-api.intside.co/users/login/',
|
email: credentials?.email,
|
||||||
data: {
|
password: credentials?.password,
|
||||||
email: credentials?.email,
|
|
||||||
password: credentials?.password,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function (response: any) {
|
|
||||||
const { user_id } = jwtDecode(response.access_token) as {
|
|
||||||
user_id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
if (error instanceof AxiosError) {
|
|
||||||
if (error.status === 401) {
|
|
||||||
throw new Error("Email ou mot de passe incorrect");
|
|
||||||
} else {
|
|
||||||
throw new Error(error.message, error);
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
throw new Error("Une erreur est survenue");
|
|
||||||
});
|
const { access_token, refresh_token } = response.data;
|
||||||
|
const { id } = jwtDecode(access_token) as { id: string };
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
email: credentials?.email,
|
||||||
|
access_token: access_token,
|
||||||
|
refresh_token: refresh_token
|
||||||
|
} as User;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Email ou mot de passe incorrect");
|
||||||
|
}
|
||||||
|
throw new Error(error.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
throw new Error("Une erreur est survenue");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
]
|
],
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, user }) {
|
||||||
|
if (user) {
|
||||||
|
token.access_token = user.access_token;
|
||||||
|
token.refresh_token = user.refresh_token;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
return {
|
||||||
|
...session,
|
||||||
|
user: {
|
||||||
|
...session.user,
|
||||||
|
...token,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secret: process.env.AUTH_SECRET ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
export { handler as GET, handler as POST };
|
||||||
|
|||||||
@ -30,6 +30,7 @@ body {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid #d1d5dc;
|
border: 1px solid #d1d5dc;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
|
color: black;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline-color: none;
|
outline-color: none;
|
||||||
@ -58,4 +59,9 @@ body {
|
|||||||
color: white;
|
color: white;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(22, 77, 185);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { FloatingLabelInputProps } from '#/types';
|
import { FloatingLabelInputProps } from '#/types';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { icons } from '#/assets/icons';
|
||||||
|
|
||||||
export default function FloatingLabelInput({
|
export default function FloatingLabelInput({
|
||||||
label,
|
label,
|
||||||
@ -47,19 +47,29 @@ export default function FloatingLabelInput({
|
|||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
{/* {showPasswordToggle && (
|
{showPasswordToggle && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
className="btn-floating text-gray-500 hover:text-gray-700 focus:outline-none"
|
className="btn-floating text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||||
>
|
>
|
||||||
{showPassword ? (
|
{showPassword ? (
|
||||||
<EyeClosedIcon size={20} />
|
<Image
|
||||||
|
src={icons.eyeSlashIcon}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EyeOpenIcon size={20} />
|
<Image
|
||||||
|
src={icons.eyeIcon}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)} */}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -81,7 +91,7 @@ export default function FloatingLabelInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative mb-9">
|
<div className="relative">
|
||||||
<label
|
<label
|
||||||
htmlFor={name}
|
htmlFor={name}
|
||||||
className="input-label text-gray-400 text-sm"
|
className="input-label text-gray-400 text-sm"
|
||||||
|
|||||||
@ -1,38 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import FloatingLabelInput from "../floatingLabelInput"
|
import FloatingLabelInput from "../floatingLabelInput"
|
||||||
import { FormProps } from "#/types"
|
import { FormProps } from "#/types"
|
||||||
|
import { FormEvent, useState } from "react"
|
||||||
|
|
||||||
export default function Form({
|
export default function Form({
|
||||||
fields,
|
fields,
|
||||||
submit,
|
submit,
|
||||||
className,
|
className,
|
||||||
child,
|
child,
|
||||||
title
|
title,
|
||||||
|
schema
|
||||||
} : FormProps) {
|
} : FormProps) {
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
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({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={className} onSubmit={submit}>
|
<form className={className} onSubmit={handleSubmit}>
|
||||||
<div className="flex justify-center text-black">
|
<div className="flex justify-center text-black">
|
||||||
<p className="text-3xl font-bold">{title}</p>
|
<p className="text-3xl font-bold">{title}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gap-2 mt-2">
|
<div className="flex flex-col gap-8 mt-2">
|
||||||
|
|
||||||
{
|
{
|
||||||
fields.map((item, index) => (
|
fields.map((item, index) => (
|
||||||
<FloatingLabelInput
|
<div key={index}>
|
||||||
key={index}
|
<FloatingLabelInput
|
||||||
label={item.label}
|
label={item.label}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
type={item.type}
|
type={item.type}
|
||||||
button={item.button}
|
button={item.button}
|
||||||
defaultValue={item.defaultValue}
|
defaultValue={item.defaultValue}
|
||||||
options={item.options}
|
options={item.options}
|
||||||
placeholder={item.placeholder}
|
placeholder={item.placeholder}
|
||||||
showPasswordToggle={item.showPasswordToggle}
|
showPasswordToggle={item.showPasswordToggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<span className="text-red-500 text-xs mt-1">{errors[item.name]}</span>
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
{child}
|
{child}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
|
import { icons } from "#/assets/icons";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
return(
|
return(
|
||||||
<div className="w-full bg-white shadow-md py-4">
|
<div className="w-full bg-white shadow-md py-4">
|
||||||
<div className="container mx-auto text-center flex items-center justify-center">
|
<div className="container mx-auto text-center flex items-center justify-center gap-2">
|
||||||
<Image
|
<Image
|
||||||
src="/file.svg"
|
src={icons.logo}
|
||||||
alt="Private Docs"
|
alt="Private Docs"
|
||||||
width={100}
|
|
||||||
height={100}
|
|
||||||
className="text-red-500 h-auto"
|
className="text-red-500 h-auto"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-2xl font-bold text-black">Private Docs</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,141 @@
|
|||||||
export default function Table() {
|
"use client";
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
getPaginationRowModel,
|
||||||
|
SortingState,
|
||||||
|
ColumnFiltersState,
|
||||||
|
getFilteredRowModel,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { useState } from "react";
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[]
|
||||||
|
data: TData[],
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Table<TData, TValue>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
pageSize = 10
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const [rowSelection, setRowSelection] = useState({})
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
|
||||||
|
state: {
|
||||||
|
rowSelection,
|
||||||
|
columnFilters,
|
||||||
|
},
|
||||||
|
initialState: {
|
||||||
|
pagination: {
|
||||||
|
pageSize: pageSize,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalPages = table.getPageCount()
|
||||||
|
const currentPage = table.getState().pagination.pageIndex + 1
|
||||||
|
|
||||||
|
const getPageNumbers = () => {
|
||||||
|
const pages = []
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<></>
|
<div>
|
||||||
|
<div className="rounded-lg border border-gray-200">
|
||||||
|
<table className="w-full overflow-x-auto rounded-lg " style={{ borderTopLeftRadius: '10px', borderTopRightRadius: '10px', }}>
|
||||||
|
<thead className="h-10">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id} className="rounded-lg">
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return(
|
||||||
|
<th key={header.id} className="bg-blue-300 p-3 text-start">
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<tr key={row.id} className={clsx('hover:bg-gray-300 border-t border-gray-200', { 'bg-gray-300': row.getIsSelected()})}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td key={cell.id} className="p-3 text-start">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={columns.length}>
|
||||||
|
Aucun résultats
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end space-x-2 py-4">
|
||||||
|
<button
|
||||||
|
className="border bg-gray-200 shadow-xs hover:bg-gray-300 hover:text-black px-3 py-1 rounded"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
Précédent
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
{getPageNumbers().map((pageNumber) => (
|
||||||
|
<button
|
||||||
|
key={pageNumber}
|
||||||
|
className={clsx(
|
||||||
|
"px-3 py-1 rounded",
|
||||||
|
pageNumber === currentPage
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "bg-gray-200 hover:bg-gray-300"
|
||||||
|
)}
|
||||||
|
onClick={() => table.setPageIndex(pageNumber - 1)}
|
||||||
|
>
|
||||||
|
{pageNumber}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="border bg-gray-200 shadow-xs hover:bg-gray-300 hover:text-black px-3 py-1 rounded"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
Suivant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
6
src/schema/loginSchema.ts
Normal file
6
src/schema/loginSchema.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const loginSchema = z.object({
|
||||||
|
email: z.string().email("Email invalide"),
|
||||||
|
password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères")
|
||||||
|
});
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { FormEventHandler, ReactNode } from "react";
|
import { FormEventHandler, ReactNode } from "react";
|
||||||
|
import { ZodSchema } from "zod";
|
||||||
|
|
||||||
export interface FloatingLabelInputProps {
|
export interface FloatingLabelInputProps {
|
||||||
label: string;
|
label: string;
|
||||||
@ -16,5 +17,6 @@ export interface FormProps {
|
|||||||
fields: FloatingLabelInputProps[],
|
fields: FloatingLabelInputProps[],
|
||||||
submit: FormEventHandler<HTMLFormElement> | undefined,
|
submit: FormEventHandler<HTMLFormElement> | undefined,
|
||||||
className: string,
|
className: string,
|
||||||
child: ReactNode
|
child: ReactNode,
|
||||||
|
schema: ZodSchema
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user