Compare commits

...

13 Commits

72 changed files with 3330 additions and 140 deletions

12
auth.d.ts vendored Normal file
View 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;
}
}

View File

@ -1,7 +1,13 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
};
export default nextConfig;

View File

@ -10,8 +10,12 @@
},
"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",
"nextjs-toploader": "^3.8.15",
"nuqs": "^2.4.1",
"react": "^19.0.0",
@ -22,6 +26,7 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",

2109
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

9
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.3 MiB

12
src/app/(auth)/layout.tsx Normal file
View File

@ -0,0 +1,12 @@
import Header from "#/components/header";
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return(
<div className="flex flex-col min-h-screen">
<Header />
<div className="flex flex-1 justify-center items-center bg-blue-100">
{children}
</div>
</div>
)
}

View File

@ -0,0 +1,33 @@
"use client";
import Form from "#/components/form/form"
import { loginSchema } from "#/schema/loginSchema"
export default function LoginPage() {
return(
<div>
<Form
title="Connexion"
className="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={undefined}
schema={loginSchema}
child={<button type="submit" className="btn-auth">Connexion</button>}
/>
</div>
)
}

129
src/app/admin/home/page.tsx Normal file
View File

@ -0,0 +1,129 @@
"use client"
import Table from "#/components/table/table"
import { ColumnDef } from "@tanstack/react-table"
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(
<>
<Table
columns={columns}
data={data}
pageSize={5}
/>
</>
)
}

43
src/app/admin/layout.tsx Normal file
View File

@ -0,0 +1,43 @@
import { ReactNode } from "react";
import "../../assets/css/admin.css"
import Sidebar from "../components/sidebar";
import Header from "../components/adminHeader";
export default function Dashboard({ children }: { children: ReactNode }) {
return (
<div className="r-flex-between">
<Sidebar />
<main className="flex-grow-1 min-w-0 pt-md-1 pt-sm-5">
<Header/>
<div className="p-4">
{children}
</div>
</main>
</div >
)
}
/*
<div className="sidebar r-m-0 d-flex flex-column pt-[16px] max-w-[90px] h-[100vh] relative ">
<div className="logo r-flex-center pt-[13px] px-[20px] ">
<icons.Logo aria-label="Logo" className="scale-95" />
</div>
<div className="nav-menu r-column-center h-max pt-[160px] r-gap-40 ">
<Link href="#" className="nav-item r-flex-center ">
<icons.HomeIcon aria-label="Home" className="nav-home scale-100" />
</Link>
<Link href="#" className="nav-item border-none r-flex-center ">
<icons.CompaniesIcon aria-label="Companies" className="scale-100 " width={24} height={24} />
</Link>
<Link href="#" className="nav-item r-flex-center ">
<icons.UserGroup aria-label="Admins" className="scale-100" width={24} height={24} />
</Link>
</div>
</div>
<div className="logout absolute bottom-[60px] left-[22px]">
<icons.Logout aria-label="Logout" />
</div>
*/

6
src/app/admin/page.tsx Normal file
View File

@ -0,0 +1,6 @@
export default function Admin () {
return(
<>
</>
)
}

View File

@ -0,0 +1,68 @@
import NextAuth, { User } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import axios from "axios";
import { jwtDecode } from "jwt-decode";
const handler = NextAuth({
providers: [
Credentials({
credentials: {
email: {},
password: {},
},
async authorize(credentials) {
try {
const response = await axios.post(
'private-docs-api.intside.co/users/login/',
{
email: credentials?.email,
password: credentials?.password,
}
)
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 };

View File

@ -0,0 +1,18 @@
import Image from "next/image";
import { icons } from "#/assets/icons"
export default function AdminHeader() {
return (
<>
<nav className="header r-flex-between px-[44px] py-[20px] ">
<p className="name text-[26px]">Bienvenue, <span>Ken B.</span> </p>
<div className="r-flex-between justify-center items-center r-gap-12">
<Image src={icons.notificationsIcon} alt="Notifications" />
<Image src={icons.profilePicture} alt="ProfilePicture" />
<Image src={icons.arrowUp} alt="arrowUp" />
</div>
</nav>
</>
)
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 582 KiB

View File

@ -0,0 +1,21 @@
import Image from "next/image";
import Link from "next/link";
interface ItemProps {
link: string;
iconSrc: string;
label: string;
isActive: boolean;
isNavHome?: boolean;
onClick: () => void;
}
export default function NavItem({ link, iconSrc, label, isActive, isNavHome, onClick }: ItemProps) {
return (
<>
<Link href={link} onClick={onClick} className={`nav-item r-flex-center ${isActive ? "active" : ""}`} >
<Image src={iconSrc} alt={label} className={`scale-100 ${isNavHome ? "nav-home" : ""}`} />
</Link>
</>
);
}

View File

@ -0,0 +1,38 @@
"use client"
import Image from "next/image";
import { icons } from "#/assets/icons"
import NavItem from "./navItem";
import { useState } from "react";
export default function Sidebar() {
const [activeItem, setActiveItem] = useState("home")
const handleNavMenu = (item:string) => {
setActiveItem(item)
console.log("active: ", item);
}
return (
<>
<div className="sidebar r-m-0 d-flex flex-column pt-[25px] max-w-[90px] h-[100vh] relative ">
<div className="logo r-flex-center px-[20px] ">
<Image src={icons.logo} alt="Logo" className="scale-95" />
</div>
<div className="nav-menu r-column-center h-max pt-[160px] r-gap-40 ">
<NavItem link="#" iconSrc={icons.homeIcon} label="Home" isNavHome={true} isActive={activeItem === "home"} onClick={() => handleNavMenu("home") } />
<NavItem link="#" iconSrc={icons.companiesIcon} label="Organizations" isActive={activeItem === "organizations"} onClick={() => handleNavMenu("organizations") } />
<NavItem link="#" iconSrc={icons.userGroup} label="Admins" isActive={activeItem === "admins"} onClick={() => handleNavMenu("admins") } />
</div>
<div className="logout absolute bottom-[40px] left-[28px]">
<button type="button" className="cursor-pointer">
<Image src={icons.logout} alt="Logout" />
</button>
</div>
</div>
</>
)
}

View File

@ -1,26 +1,67 @@
@import "tailwindcss";
:root {
--foreground: #04060F;
--background: #ffffff;
--foreground: #171717;
--primary: #246BFD;
--cinder: #E7E5E4;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
[ data-theme="dark"] {
--foreground: #04060F;
--background: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--foreground: #04060F;
--background: #ffffff;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: Inter, sans-serif;
}
.input-form {
width: 100%;
padding: 12px;
border: 1px solid #d1d5dc;
border-radius: 9999px;
color: black;
&:focus {
outline-color: none;
}
}
.input-label {
position: absolute;
left: 12px;
top: -0.45rem;
background-color: white;
padding-inline: 4px;
z-index: 1;
}
.btn-floating {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
}
.btn-auth {
border-radius: 9999px;
background-color: #246BFD;
color: white;
width: 100%;
padding: 8px;
cursor: pointer;
&:hover {
background-color: rgb(22, 77, 185);
}
}

View File

@ -1,21 +1,14 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Inter } from "next/font/google";
import "./globals.css";
import NextTopLoader from "nextjs-toploader";
import "../assets/css/ruben-ui.css"
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Private Docs",
description: "Un application de gestion de documents",
description: "L'appli de gestion de documents par excellence !",
};
export default function RootLayout({
@ -25,10 +18,12 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<NextTopLoader />
<head>
<link rel="icon" href="/favicon.svg" />
<link rel="favicon.svg" href="/favicon.svg" />
</head>
<body className={inter.className}>
<NextTopLoader color="#246BFD" shadow="0" />
{children}
</body>
</html>

View File

@ -1,103 +1,10 @@
import Image from "next/image";
import Link from "next/link";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
<Link href="/admin">See the admin page</Link>
</div>
);
}

28
src/assets/css/admin.css Normal file
View File

@ -0,0 +1,28 @@
.sidebar{
border-right: 1px solid var(--cinder);
}
.nav-item .nav-home{
margin-bottom: -10px;
}
.nav-item{
width: 100%;
height: max-content;
}
.nav-item.active{
border-right: 2px solid var(--primary);
}
.nav-item svg{
color: var(--primary)!important;
background-color: var(--primary)!important;
fill: var(--primary)!important;
stroke: var(--primary)!important
}
.nav-home{
margin-top: -11px;
margin-bottom: -11px;
}

160
src/assets/css/ruben-ui.css Normal file
View File

@ -0,0 +1,160 @@
/* Extra Small (XS) */
.r-p-0 {
padding: 0;
}
.r-m-0 {
margin: 0;
}
.r-p-m-0{
padding: 0;
margin: 0;
}
.r-p-0auto {
padding: 0 auto;
}
.r-m-0auto {
margin: 0 auto;
}
.r-flex {
display: flex;
}
.r-flex-center {
display: flex;
justify-content: center;
}
.r-flex-between {
display: flex;
justify-content: space-between;
}
.r-flex-around {
display: flex;
justify-content: space-around;
}
.r-flex-evenly {
display: flex;
justify-content: space-evenly;
}
.r-flex-column {
display: flex;
flex-direction: column;
}
.r-column-center,.r-column-md-row, .r-column-lg-row, .r-column-xl-row {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.r-row-center, .r-row-md-column, .r-row-lg-column, .r-row-xl-column {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.r-gap-2{
gap: 2px;
}
.r-gap-4{
gap: 4px;
}
.r-gap-6{
gap: 6px;
}
.r-gap-8{
gap: 8px;
}
.r-gap-10{
gap: 10px;
}
.r-gap-12{
gap: 12px;
}
.r-gap-14{
gap: 14px;
}
.r-gap-16{
gap: 16px;
}
.r-gap-18{
gap: 18px;
}
.r-gap-20{
gap: 20px;
}
.r-gap-22{
gap: 22px;
}
.r-gap-24{
gap: 24px;
}
.r-gap-32{
gap: 32px;
}
.r-gap-36{
gap: 36px;
}
.r-gap-40{
gap: 40px;
}
.r-gap-42{
gap: 42px;
}
.r-gap-50{
gap: 50px;
}
.r-gap-60{
gap: 60px;
}
.r-gap-70{
gap: 70px;
}
/* Small (SM) */
@media (min-width: 640px) {
/* Styles for small devices and up */
}
/* Medium (MD) */
@media (min-width: 768px) {
.r-column-md-row {
flex-direction: row;
}
.r-row-md-column{
flex-direction: column;
}
}
/* Large (LG) */
@media (min-width: 1024px) {
.r-column-lg-row {
flex-direction: row;
}
.r-row-lg-column{
flex-direction: column;
}
}
/* Extra Large (XL) */
@media (min-width: 1280px) {
.r-column-xl-row {
flex-direction: row;
}
.r-row-xl-column{
flex-direction: column;
}
}
/* 2XL (XXL) */
@media (min-width: 1536px) {
/* Styles for very large screens */
}

View File

Before

Width:  |  Height:  |  Size: 927 B

After

Width:  |  Height:  |  Size: 927 B

View File

Before

Width:  |  Height:  |  Size: 600 B

After

Width:  |  Height:  |  Size: 600 B

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 592 B

After

Width:  |  Height:  |  Size: 592 B

View File

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 286 B

View File

Before

Width:  |  Height:  |  Size: 535 B

After

Width:  |  Height:  |  Size: 535 B

View File

@ -1,4 +1,4 @@
<svg width="48" height="49" viewBox="0 0 48 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 48 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.02 15.34L15.63 19.54C14.73 20.24 14 21.73 14 22.86V30.27C14 32.59 15.89 34.49 18.21 34.49H29.79C32.11 34.49 34 32.59 34 30.28V23C34 21.79 33.19 20.24 32.2 19.55L26.02 15.22C24.62 14.24 22.37 14.29 21.02 15.34Z" stroke="#246BFD" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24 30.49V27.49" stroke="#246BFD" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 523 B

After

Width:  |  Height:  |  Size: 500 B

View File

Before

Width:  |  Height:  |  Size: 191 B

After

Width:  |  Height:  |  Size: 191 B

View File

Before

Width:  |  Height:  |  Size: 191 B

After

Width:  |  Height:  |  Size: 191 B

View File

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 207 B

View File

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 331 B

View File

Before

Width:  |  Height:  |  Size: 244 B

After

Width:  |  Height:  |  Size: 244 B

View File

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 10.72V19.5C19.5 21.5 19 22.5 16.5 22.5H7.5C5 22.5 4.5 21.5 4.5 19.5V10.72" stroke="#9FA8BC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 2.5H19C21 2.5 22 3.5 22 5.5V7.5C22 9.5 21 10.5 19 10.5H5C3 10.5 2 9.5 2 7.5V5.5C2 3.5 3 2.5 5 2.5Z" stroke="#9FA8BC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.1799 14.5H13.8199" stroke="#9FA8BC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 592 B

View File

@ -0,0 +1,3 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 11L1 6L6 1" stroke="#9FA8BC" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 191 B

View File

@ -0,0 +1,3 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 11L6 6L1 1" stroke="#9FA8BC" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 191 B

View File

@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 22H5C3 22 2 21 2 19V11C2 9 3 8 5 8H10V19C10 21 11 22 13 22Z" stroke="#9FA8BC" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.11 4C10.03 4.3 10 4.63 10 5V8H5V6C5 4.9 5.9 4 7 4H10.11Z" stroke="#9FA8BC" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 8V13" stroke="#9FA8BC" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 8V13" stroke="#9FA8BC" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17 17H15C14.45 17 14 17.45 14 18V22H18V18C18 17.45 17.55 17 17 17Z" stroke="#9FA8BC" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 13V17" stroke="#9FA8BC" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 19V5C10 3 11 2 13 2H19C21 2 22 3 22 5V19C22 21 21 22 19 22H13C11 22 10 21 10 19Z" stroke="#9FA8BC" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3C4.46957 3 3.96086 3.21071 3.58579 3.58579C3.21071 3.96086 3 4.46957 3 5V19C3 19.5304 3.21071 20.0391 3.58579 20.4142C3.96086 20.7893 4.46957 21 5 21H19C19.5304 21 20.0391 20.7893 20.4142 20.4142C20.7893 20.0391 21 19.5304 21 19V5C21 4.46957 20.7893 3.96086 20.4142 3.58579C20.0391 3.21071 19.5304 3 19 3H5ZM16.95 9.796C17.1376 9.60849 17.2431 9.35412 17.2432 9.08885C17.2433 8.82358 17.138 8.56914 16.9505 8.3815C16.763 8.19386 16.5086 8.08839 16.2434 8.0883C15.9781 8.0882 15.7236 8.19349 15.536 8.381L10.586 13.331L8.465 11.21C8.37216 11.1171 8.26192 11.0434 8.14059 10.9931C8.01926 10.9428 7.8892 10.9168 7.75785 10.9168C7.49258 10.9167 7.23814 11.022 7.0505 11.2095C6.86286 11.397 6.75739 11.6514 6.7573 11.9166C6.7572 12.1819 6.86249 12.4364 7.05 12.624L9.808 15.382C9.91015 15.4842 10.0314 15.5653 10.1649 15.6206C10.2984 15.6759 10.4415 15.7044 10.586 15.7044C10.7305 15.7044 10.8736 15.6759 11.0071 15.6206C11.1406 15.5653 11.2618 15.4842 11.364 15.382L16.95 9.796Z" fill="#246BFD"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0091 12.1536L15.3973 17.5357C15.6833 17.8213 16.0711 17.9818 16.4754 17.9818C16.8797 17.9818 17.2675 17.8213 17.5535 17.5357C17.8394 17.2501 18 16.8628 18 16.4589C18 16.055 17.8394 15.6677 17.5535 15.3821L12.1632 10L17.5524 4.61791C17.6939 4.4765 17.8062 4.30863 17.8827 4.12389C17.9593 3.93916 17.9987 3.74117 17.9986 3.54123C17.9986 3.34129 17.9591 3.14332 17.8825 2.95862C17.8058 2.77392 17.6935 2.60611 17.5519 2.46476C17.4104 2.32342 17.2423 2.21131 17.0574 2.13484C16.8724 2.05837 16.6742 2.01904 16.474 2.01909C16.2739 2.01914 16.0757 2.05856 15.8908 2.13512C15.7058 2.21167 15.5378 2.32386 15.3963 2.46527L10.0091 7.84736L4.62088 2.46527C4.48035 2.31981 4.31223 2.20375 4.12632 2.12388C3.94041 2.04401 3.74044 2.00192 3.53807 2.00006C3.3357 1.99821 3.13499 2.03664 2.94764 2.1131C2.7603 2.18955 2.59008 2.30251 2.44691 2.44539C2.30374 2.58826 2.19049 2.75818 2.11377 2.94524C2.03705 3.13229 1.99839 3.33274 2.00005 3.53488C2.00171 3.73702 2.04366 3.9368 2.12345 4.12258C2.20324 4.30835 2.31927 4.47639 2.46477 4.61689L7.85504 10L2.46579 15.3831C2.32028 15.5236 2.20426 15.6917 2.12447 15.8774C2.04468 16.0632 2.00273 16.263 2.00107 16.4651C1.9994 16.6673 2.03806 16.8677 2.11478 17.0548C2.19151 17.2418 2.30476 17.4117 2.44793 17.5546C2.5911 17.6975 2.76132 17.8104 2.94866 17.8869C3.136 17.9634 3.33671 18.0018 3.53908 17.9999C3.74145 17.9981 3.94142 17.956 4.12733 17.8761C4.31324 17.7963 4.48137 17.6802 4.62189 17.5347L10.0091 12.1536Z" fill="#9FA8BC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 750 B

After

Width:  |  Height:  |  Size: 750 B

View File

Before

Width:  |  Height:  |  Size: 855 B

After

Width:  |  Height:  |  Size: 855 B

View File

Before

Width:  |  Height:  |  Size: 549 B

After

Width:  |  Height:  |  Size: 549 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 699 B

After

Width:  |  Height:  |  Size: 699 B

View File

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 419 B

170
src/assets/icons/index.ts Normal file
View File

@ -0,0 +1,170 @@
import addIcon from "./add.svg"
import calendarIcon from "./Calendar Mark.svg"
import docummentTextIcon from "./document-text.svg"
import folderIcon from "./Document.svg"
import editIcon from "./edit-2.svg"
import gridIcon from "./element-3-white.svg"
import gridBlueIcon from "./element-3.svg"
import profilePicture from "./Ellipse 2.svg"
import eyeIcon from "./eye.svg"
import eyeSlashIcon from "./eye-slash.svg"
import checkboxchedIcon from "./checked.svg"
import crossIcon from "./cross.svg"
import addBlueIcon from "./icon-add.svg"
import archivesIcon from "./archives.svg"
import notificationsIcon from "./notifications.svg"
import timerIcon from "./Line Duotone.svg"
import logo from "./logo.svg"
import logoutRed from "./logout-red.svg"
import logout from "./logout.svg"
import maximizeIcon from "./maximize-3.svg"
import menuIcon from "./Menu Dots.svg"
import messagesIcon from "./message.svg"
import homeIcon from "./NavItem.svg"
import companiesIcon from "./buildings.svg"
import arrowLeft from "./arrowLeft.svg"
import arrowRight from "./arrowLeft.svg"
import filesIcon from "./ph_files.svg"
import pdfIcon from "./prime_file-pdf.svg"
import wordIcon from "./prime_file-word.svg"
import userIcon from "./profile.svg"
import userGroup from "./profile-2user.svg"
import userGroupBlue from "./profile-2user-blue.svg"
import rectanagle from "./Rectangle.svg"
import searchIcon from "./Search.svg"
import settingsIcon from "./setting-2.svg"
import filterIcon from "./setting-3.svg"
import shareIcon from "./share.svg"
import starIcon from "./star.svg"
import arrowUp from "./Vector.svg"
export const icons = {
addIcon,
calendarIcon,
docummentTextIcon,
folderIcon,
editIcon,
gridIcon,
gridBlueIcon,
profilePicture,
eyeIcon,
eyeSlashIcon,
checkboxchedIcon,
crossIcon,
addBlueIcon,
archivesIcon,
notificationsIcon,
timerIcon,
logo,
companiesIcon,
logout,
logoutRed,
maximizeIcon,
menuIcon,
messagesIcon,
homeIcon,
arrowLeft,
arrowRight,
filesIcon,
pdfIcon,
wordIcon,
userIcon,
userGroup,
userGroupBlue,
rectanagle,
searchIcon,
settingsIcon,
filterIcon,
shareIcon,
starIcon,
arrowUp
}
/*
import AddIcon from "./add.svg";
import CalendarIcon from "./Calendar Mark.svg";
import DocummentTextIcon from "./document-text.svg";
import FolderIcon from "./Document.svg";
import EditIcon from "./edit-2.svg";
import GridIcon from "./element-3-white.svg";
import GridBlueIcon from "./element-3.svg";
import ProfilePicture from "./Ellipse 2.svg";
import EyeIcon from "./eye.svg";
import EyeSlashIcon from "./eye-slash.svg";
import CheckboxchedIcon from "./checked.svg";
import CrossIcon from "./cross.svg";
import AddBlueIcon from "./icon-add.svg";
import ArchivesIcon from "./archives.svg";
import NotificationsIcon from "./notifications.svg";
import TimerIcon from "./Line Duotone.svg";
import Logo from "./logo.svg";
import LogoutRed from "./logout-red.svg";
import Logout from "./logout.svg";
import MaximizeIcon from "./maximize-3.svg";
import MenuIcon from "./Menu Dots.svg";
import MessagesIcon from "./message.svg";
import HomeIcon from "./NavItem.svg";
import CompaniesIcon from "./buildings.svg";
import ArrowLeft from "./arrowLeft.svg";
import ArrowRight from "./arrowLeft.svg";
import FilesIcon from "./ph_files.svg";
import PdfIcon from "./prime_file-pdf.svg";
import WordIcon from "./prime_file-word.svg";
import UserIcon from "./profile.svg";
import UserGroup from "./profile-2user.svg";
import UserGroupBlue from "./profile-2user-blue.svg";
import Rectanagle from "./Rectangle.svg";
import SearchIcon from "./Search.svg";
import SettingsIcon from "./setting-2.svg";
import FilterIcon from "./setting-3.svg";
import ShareIcon from "./share.svg";
import StarIcon from "./star.svg";
import ArrowUp from "./Vector.svg";
export const icons = {
AddIcon,
CalendarIcon,
DocummentTextIcon,
FolderIcon,
EditIcon,
GridIcon,
GridBlueIcon,
ProfilePicture,
EyeIcon,
EyeSlashIcon,
CheckboxchedIcon,
CrossIcon,
AddBlueIcon,
ArchivesIcon,
NotificationsIcon,
TimerIcon,
Logo,
CompaniesIcon,
Logout,
LogoutRed,
MaximizeIcon,
MenuIcon,
MessagesIcon,
HomeIcon,
ArrowLeft,
ArrowRight,
FilesIcon,
PdfIcon,
WordIcon,
UserIcon,
UserGroup,
UserGroupBlue,
Rectanagle,
SearchIcon,
SettingsIcon,
FilterIcon,
ShareIcon,
StarIcon,
ArrowUp
};
*/

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 667 B

After

Width:  |  Height:  |  Size: 667 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.8999 7.55999C9.2099 3.95999 11.0599 2.48999 15.1099 2.48999H15.2399C19.7099 2.48999 21.4999 4.27999 21.4999 8.74999V15.27C21.4999 19.74 19.7099 21.53 15.2399 21.53H15.1099C11.0899 21.53 9.2399 20.08 8.9099 16.54" stroke="#9FA8BC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.0001 12H3.62012" stroke="#9FA8BC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.85 8.65002L2.5 12L5.85 15.35" stroke="#9FA8BC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 655 B

View File

Before

Width:  |  Height:  |  Size: 595 B

After

Width:  |  Height:  |  Size: 595 B

View File

Before

Width:  |  Height:  |  Size: 989 B

After

Width:  |  Height:  |  Size: 989 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 9C11.59 9 11.25 8.71843 11.25 8.37888V5.62112C11.25 5.28157 11.59 5 12 5C12.41 5 12.75 5.28157 12.75 5.62112V8.37888C12.75 8.72671 12.41 9 12 9Z" fill="#9FA8BC"/>
<path d="M12.0161 19.6454C9.61036 19.6454 7.21398 19.2668 4.92949 18.5096C4.08097 18.2326 3.43758 17.6324 3.15785 16.8753C2.87811 16.1181 2.97136 15.2502 3.42825 14.493L4.61246 12.5355C4.87354 12.1015 5.10666 11.289 5.10666 10.7811V8.84208C5.10666 5.06555 8.20237 2 12.0161 2C15.8298 2 18.9255 5.06555 18.9255 8.84208V10.7811C18.9255 11.2797 19.1586 12.1015 19.4197 12.5355L20.6039 14.493C21.0421 15.2132 21.1167 16.072 20.8277 16.8568C20.5386 17.6417 19.9046 18.2419 19.1027 18.5096C16.8182 19.276 14.4218 19.6454 12.0161 19.6454ZM12.0161 3.39427C8.9763 3.39427 6.50532 5.84117 6.50532 8.85131V10.7904C6.50532 11.5383 6.20694 12.6186 5.81531 13.2557L4.63111 15.2225C4.38867 15.6195 4.33273 16.0443 4.47259 16.4136C4.61246 16.783 4.92949 17.06 5.37706 17.2077C9.66631 18.6204 14.3845 18.6204 18.6737 17.2077C19.0747 17.0784 19.3824 16.783 19.5223 16.3951C19.6714 16.0073 19.6248 15.5826 19.4104 15.2225L18.2262 13.265C17.8345 12.6278 17.5361 11.5475 17.5361 10.7996V8.86055C17.5268 5.84117 15.0558 3.39427 12.0161 3.39427Z" fill="#9FA8BC"/>
<path d="M12 22C10.951 22 9.92157 21.5937 9.17647 20.8919C8.43137 20.1902 8 19.2207 8 18.2327H9.47059C9.47059 18.8605 9.7451 19.47 10.2157 19.9132C10.6863 20.3564 11.3333 20.6149 12 20.6149C13.3922 20.6149 14.5294 19.5438 14.5294 18.2327H16C16 20.3102 14.2059 22 12 22Z" fill="#9FA8BC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 737 B

After

Width:  |  Height:  |  Size: 737 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 806 B

View File

@ -0,0 +1,104 @@
"use client";
import { FloatingLabelInputProps } from '#/types';
import React, { useState } from 'react';
import Image from 'next/image';
import { icons } from '#/assets/icons';
export default function FloatingLabelInput({
label,
placeholder,
type,
options,
button,
showPasswordToggle = false,
name,
defaultValue
}: FloatingLabelInputProps) {
const [showPassword, setShowPassword] = useState(false);
const renderInput = () => {
switch(type) {
case 'select':
return (
<div className="relative w-full">
<select
className="input-form focus:ring-2 focus:ring-blue-500"
required
name={name}
defaultValue={defaultValue}
>
{options?.map((option, index) => (
<option key={index} value={option}>{option}</option>
))}
</select>
{button && <div className="btn-floating">{button}</div>}
</div>
);
case 'password':
return (
<div className="relative w-full">
<input
name={name}
type={showPassword ? "text" : "password"}
placeholder={placeholder}
className="input-form focus:ring-2 focus:ring-blue-500 pr-10"
defaultValue={defaultValue}
required
/>
{showPasswordToggle && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="btn-floating text-gray-500 hover:text-gray-700 focus:outline-none"
>
{showPassword ? (
<Image
src={icons.eyeSlashIcon}
width={20}
height={20}
alt=''
/>
) : (
<Image
src={icons.eyeIcon}
width={20}
height={20}
alt=''
/>
)}
</button>
)}
</div>
);
default:
return (
<div className="relative w-full">
<input
type={type}
placeholder={placeholder}
className="input-form focus:ring-2 focus:ring-blue-500"
required
name={name}
defaultValue={defaultValue}
/>
{button && <div className="absolute right-0 top-1/2 transform -translate-y-1/2">{button}</div>}
</div>
);
}
};
return (
<div className="relative">
<label
htmlFor={name}
className="input-label text-gray-400 text-sm"
>
{label}
</label>
{renderInput()}
</div>
);
}

View File

@ -0,0 +1,74 @@
"use client";
import FloatingLabelInput from "../floatingLabelInput"
import { FormProps } from "#/types"
import { FormEvent, useState } from "react"
export default function Form({
fields,
submit,
className,
child,
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={handleSubmit}>
<div className="flex justify-center text-black">
<p className="text-3xl font-bold">{title}</p>
</div>
<div className="flex flex-col gap-8 mt-2">
{
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>
))
}
{child}
</div>
</form>
)
}

17
src/components/header.tsx Normal file
View File

@ -0,0 +1,17 @@
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>
)
}

View File

@ -0,0 +1,141 @@
"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(
<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>
)
}

View 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")
});

22
src/types/index.ts Normal file
View File

@ -0,0 +1,22 @@
import { FormEventHandler, ReactNode } from "react";
import { ZodSchema } from "zod";
export interface FloatingLabelInputProps {
label: string;
placeholder?: string;
type: 'text' | 'password' | 'select' | 'email' | 'number';
options?: string[];
button?: React.ReactNode;
showPasswordToggle?: boolean;
name: string;
defaultValue?: string;
}
export interface FormProps {
title?: string,
fields: FloatingLabelInputProps[],
submit: FormEventHandler<HTMLFormElement> | undefined,
className: string,
child: ReactNode,
schema: ZodSchema
}