feat: add reusable table components
This commit is contained in:
parent
e967d9d8ac
commit
42b0e8b3a0
@ -10,7 +10,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"axios": "^1.8.4",
|
||||
"clsx": "^2.1.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "15.2.3",
|
||||
"next-auth": "^4.24.11",
|
||||
|
||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@ -11,9 +11,15 @@ importers:
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.69.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:
|
||||
specifier: ^1.8.4
|
||||
version: 1.8.4
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
jwt-decode:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@ -509,6 +515,17 @@ packages:
|
||||
peerDependencies:
|
||||
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'}
|
||||
|
||||
'@tybys/wasm-util@0.9.0':
|
||||
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
|
||||
|
||||
@ -762,6 +779,10 @@ packages:
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@ -2300,6 +2321,14 @@ snapshots:
|
||||
'@tanstack/query-core': 5.69.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': {}
|
||||
|
||||
'@tybys/wasm-util@0.9.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@ -2594,6 +2623,8 @@ snapshots:
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Form from "#/components/form/form"
|
||||
import { loginSchema } from "#/schema/loginSchema"
|
||||
|
||||
export default function LoginPage() {
|
||||
return(
|
||||
@ -17,10 +20,12 @@ export default function LoginPage() {
|
||||
label: "Password",
|
||||
name: "password",
|
||||
type: "password",
|
||||
placeholder: "Enter votre mot de passe"
|
||||
placeholder: "Enter votre mot de passe",
|
||||
showPasswordToggle: true
|
||||
}
|
||||
]}
|
||||
submit={undefined}
|
||||
schema={loginSchema}
|
||||
child={<button type="submit" className="btn-auth">Connexion</button>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -31,6 +31,7 @@ body {
|
||||
padding: 12px;
|
||||
border: 1px solid #d1d5dc;
|
||||
border-radius: 9999px;
|
||||
color: black;
|
||||
|
||||
&:focus {
|
||||
outline-color: none;
|
||||
@ -59,4 +60,9 @@ body {
|
||||
color: white;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(22, 77, 185);
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
import { FloatingLabelInputProps } from '#/types';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
|
||||
import Image from 'next/image';
|
||||
import { icons } from '#/assets/icons';
|
||||
|
||||
export default function FloatingLabelInput({
|
||||
label,
|
||||
@ -47,19 +47,29 @@ export default function FloatingLabelInput({
|
||||
defaultValue={defaultValue}
|
||||
required
|
||||
/>
|
||||
{/* {showPasswordToggle && (
|
||||
{showPasswordToggle && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="btn-floating text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||
>
|
||||
{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>
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -81,7 +91,7 @@ export default function FloatingLabelInput({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative mb-9">
|
||||
<div className="relative">
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="input-label text-gray-400 text-sm"
|
||||
|
||||
@ -1,25 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import FloatingLabelInput from "../floatingLabelInput"
|
||||
import { FormProps } from "#/types"
|
||||
import { FormEvent, useState } from "react"
|
||||
|
||||
export default function Form({
|
||||
fields,
|
||||
submit,
|
||||
className,
|
||||
child,
|
||||
title
|
||||
title,
|
||||
schema
|
||||
} : 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 (
|
||||
<form className={className} onSubmit={submit}>
|
||||
<form className={className} onSubmit={handleSubmit}>
|
||||
<div className="flex justify-center text-black">
|
||||
<p className="text-3xl font-bold">{title}</p>
|
||||
</div>
|
||||
|
||||
<div className="gap-2 mt-2">
|
||||
<div className="flex flex-col gap-8 mt-2">
|
||||
|
||||
{
|
||||
fields.map((item, index) => (
|
||||
<div key={index}>
|
||||
<FloatingLabelInput
|
||||
key={index}
|
||||
label={item.label}
|
||||
name={item.name}
|
||||
type={item.type}
|
||||
@ -29,10 +61,13 @@ export default function Form({
|
||||
placeholder={item.placeholder}
|
||||
showPasswordToggle={item.showPasswordToggle}
|
||||
/>
|
||||
|
||||
<span className="text-red-500 text-xs mt-1">{errors[item.name]}</span>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{child}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
)
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
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">
|
||||
<div className="container mx-auto text-center flex items-center justify-center gap-2">
|
||||
<Image
|
||||
src="/file.svg"
|
||||
src={icons.logo}
|
||||
alt="Private Docs"
|
||||
width={100}
|
||||
height={100}
|
||||
className="text-red-500 h-auto"
|
||||
/>
|
||||
<p className="text-2xl font-bold text-black">Private Docs</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,5 +1,108 @@
|
||||
export default function Table() {
|
||||
"use client";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
getPaginationRowModel,
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
getFilteredRowModel,
|
||||
} from "@tanstack/react-table"
|
||||
import { useState } from "react";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
}
|
||||
|
||||
export default function Table<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
|
||||
state: {
|
||||
rowSelection,
|
||||
columnFilters,
|
||||
},
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
})
|
||||
|
||||
|
||||
function clsx(arg0: string, arg1: { 'bg-gray-300': boolean; }): string | undefined {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
||||
|
||||
return(
|
||||
<></>
|
||||
<div>
|
||||
<div className="rounded-md border">
|
||||
<table>
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return(
|
||||
<th key={header.id}>
|
||||
{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', { 'bg-gray-300': row.getIsSelected()})}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)
|
||||
: (
|
||||
<tr>
|
||||
<td>
|
||||
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"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Précédent
|
||||
</button>
|
||||
<button
|
||||
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 { ZodSchema } from "zod";
|
||||
|
||||
export interface FloatingLabelInputProps {
|
||||
label: string;
|
||||
@ -16,5 +17,6 @@ export interface FormProps {
|
||||
fields: FloatingLabelInputProps[],
|
||||
submit: FormEventHandler<HTMLFormElement> | undefined,
|
||||
className: string,
|
||||
child: ReactNode
|
||||
child: ReactNode,
|
||||
schema: ZodSchema
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user