diff --git a/ui/package.json b/ui/package.json
index 1fce942..b635a46 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -43,6 +43,8 @@
"dependencies": {
"@react-stately/calendar": "^3.7.1",
"@tailwindcss/vite": "^4.0.14",
+ "@types/react-router-dom": "^5.3.3",
+ "react-router-dom": "^7.3.0",
"react-stately": "^3.36.1"
}
}
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index aab1cad..bb7cc4f 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -14,6 +14,12 @@ importers:
'@tailwindcss/vite':
specifier: ^4.0.14
version: 4.0.14(vite@6.2.2(jiti@2.4.2)(lightningcss@1.29.2))
+ '@types/react-router-dom':
+ specifier: ^5.3.3
+ version: 5.3.3
+ react-router-dom:
+ specifier: ^7.3.0
+ version: 7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react-stately:
specifier: ^3.36.1
version: 3.36.1(react@19.0.0)
@@ -1259,12 +1265,18 @@ packages:
'@types/babel__traverse@7.20.6':
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
+ '@types/cookie@0.6.0':
+ resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
'@types/gensync@1.0.4':
resolution: {integrity: sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==}
+ '@types/history@4.7.11':
+ resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -1273,6 +1285,12 @@ packages:
peerDependencies:
'@types/react': ^19.0.0
+ '@types/react-router-dom@5.3.3':
+ resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==}
+
+ '@types/react-router@5.1.20':
+ resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
+
'@types/react@19.0.10':
resolution: {integrity: sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==}
@@ -1458,6 +1476,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie@1.0.2:
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+ engines: {node: '>=18'}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1980,6 +2002,23 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
+ react-router-dom@7.3.0:
+ resolution: {integrity: sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.3.0:
+ resolution: {integrity: sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react-stately@3.36.1:
resolution: {integrity: sha512-H9kiGAylNec/iE5qk7qQLV1cvtSAIVq3mgt87zx2EA+f+/sYy2oBtchFPaDiBf/m7xMEKf0Fr9zSLU6G99xQ8g==}
peerDependencies:
@@ -2034,6 +2073,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -2111,6 +2153,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ turbo-stream@2.4.0:
+ resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -3750,16 +3795,31 @@ snapshots:
dependencies:
'@babel/types': 7.26.8
+ '@types/cookie@0.6.0': {}
+
'@types/estree@1.0.6': {}
'@types/gensync@1.0.4': {}
+ '@types/history@4.7.11': {}
+
'@types/json-schema@7.0.15': {}
'@types/react-dom@19.0.4(@types/react@19.0.10)':
dependencies:
'@types/react': 19.0.10
+ '@types/react-router-dom@5.3.3':
+ dependencies:
+ '@types/history': 4.7.11
+ '@types/react': 19.0.10
+ '@types/react-router': 5.1.20
+
+ '@types/react-router@5.1.20':
+ dependencies:
+ '@types/history': 4.7.11
+ '@types/react': 19.0.10
+
'@types/react@19.0.10':
dependencies:
csstype: 3.1.3
@@ -3969,6 +4029,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie@1.0.2: {}
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -4478,6 +4540,22 @@ snapshots:
react-refresh@0.14.2: {}
+ react-router-dom@7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ react-router: 7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+
+ react-router@7.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ '@types/cookie': 0.6.0
+ cookie: 1.0.2
+ react: 19.0.0
+ set-cookie-parser: 2.7.1
+ turbo-stream: 2.4.0
+ optionalDependencies:
+ react-dom: 19.0.0(react@19.0.0)
+
react-stately@3.36.1(react@19.0.0):
dependencies:
'@react-stately/calendar': 3.7.1(react@19.0.0)
@@ -4560,6 +4638,8 @@ snapshots:
semver@7.7.1: {}
+ set-cookie-parser@2.7.1: {}
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -4616,6 +4696,8 @@ snapshots:
tslib@2.8.1: {}
+ turbo-stream@2.4.0: {}
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index bee3eef..4751792 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -2,6 +2,9 @@ import { Button } from "./ui-components/button";
import { useState, useEffect } from "react";
import { twMerge } from "tailwind-merge";
import { Header } from "./ui-components/header";
+import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
+import { LoginPage } from "./pages/login";
+import { SignUpPage } from "./pages/signup";
type Theme = "dark" | "light" | "system";
@@ -24,403 +27,419 @@ export const App = () => {
}, [theme]);
return (
-
-
-
-
-
- Maîtrisez vos dépenses de chantier
-
-
- XTablo aide les entreprises du BTP à suivre, analyser et optimiser
- leurs dépenses de projet. Obtenez des insights en temps réel sur vos
- coûts et prenez de meilleures décisions financières. Mise en place
- en moins de 48h.
-
-
-
- Démarrer gratuitement
-
-
-
-
-
-
- Fait confiance par les entreprises du BTP
-
-
-
-
- "XTablo a révolutionné notre gestion des dépenses. Nous
- pouvons maintenant suivre chaque euro en temps réel et prendre
- de meilleures décisions pour nos projets de construction."
-
-
-
-
-
- Michel Dubois
-
-
- Chef de Projet
-
-
-
-
-
-
-
- "Les fonctionnalités de suivi des dépenses sont exactement ce
- dont nous avions besoin. Cela nous a permis de réduire nos
- coûts de 15% et d'améliorer significativement nos marges."
-
-
-
-
-
- Sophie Martin
-
-
- Directrice Financière
-
-
-
-
-
-
-
- "La gestion des dépenses sur plusieurs chantiers n'a jamais
- été aussi simple. XTablo nous donne une visibilité totale sur
- nos coûts."
-
-
-
-
-
- David Laurent
-
-
- Chef de Chantier
-
-
-
-
-
-
-
-
-
- Des tarifs adaptés à vos besoins
-
-
-
-
- Starter
-
-
-
- 12€
-
-
- /mois
-
-
-
-
-
-
-
- Jusqu'à 5 chantiers
-
-
-
-
-
- Suivi des dépenses en temps réel
-
-
-
-
-
- Rapports mensuels
-
-
-
-
-
- Support par email
-
-
-
- Commencer
-
-
-
-
-
-
- Plus populaire
-
-
-
- Pro
-
-
-
- 25€
-
-
- /mois
-
-
-
-
-
-
-
- Chantiers illimités
-
-
-
-
-
- Suivi des dépenses en temps réel
-
-
-
-
-
- Rapports personnalisés
-
-
-
-
-
- Support prioritaire
-
-
-
-
-
- API disponible
-
-
-
- Commencer
-
-
-
-
-
-
-
-
-
+
-
-
- © {new Date().getFullYear()} XTablo. Tous droits réservés.
-
-
- XTablo est une marque déposée. Les logos et noms de marques sont des
- marques déposées de leurs propriétaires respectifs.
-
-
-
-
-
+
+
+
+
+
+
+ Maîtrisez vos dépenses de chantier
+
+
+ XTablo aide les entreprises du BTP à suivre, analyser et
+ optimiser leurs dépenses de projet. Obtenez des insights
+ en temps réel sur vos coûts et prenez de meilleures
+ décisions financières. Mise en place en moins de 48h.
+
+
+
+ Démarrer gratuitement
+
+
+
+
+
+
+ Fait confiance par les entreprises du BTP
+
+
+
+
+ "XTablo a révolutionné notre gestion des dépenses.
+ Nous pouvons maintenant suivre chaque euro en temps
+ réel et prendre de meilleures décisions pour nos
+ projets de construction."
+
+
+
+
+
+ Michel Dubois
+
+
+ Chef de Projet
+
+
+
+
+
+
+
+ "Les fonctionnalités de suivi des dépenses sont
+ exactement ce dont nous avions besoin. Cela nous a
+ permis de réduire nos coûts de 15% et d'améliorer
+ significativement nos marges."
+
+
+
+
+
+ Sophie Martin
+
+
+ Directrice Financière
+
+
+
+
+
+
+
+ "La gestion des dépenses sur plusieurs chantiers n'a
+ jamais été aussi simple. XTablo nous donne une
+ visibilité totale sur nos coûts."
+
+
+
+
+
+ David Laurent
+
+
+ Chef de Chantier
+
+
+
+
+
+
+
+
+
+ Des tarifs adaptés à vos besoins
+
+
+
+
+ Starter
+
+
+
+ 12€
+
+
+ /mois
+
+
+
+
+
+
+
+ Jusqu'à 5 chantiers
+
+
+
+
+
+ Suivi des dépenses en temps réel
+
+
+
+
+
+ Rapports mensuels
+
+
+
+
+
+ Support par email
+
+
+
+ Commencer
+
+
+
+
+
+
+ Plus populaire
+
+
+
+ Pro
+
+
+
+ 25€
+
+
+ /mois
+
+
+
+
+
+
+
+ Chantiers illimités
+
+
+
+
+
+ Suivi des dépenses en temps réel
+
+
+
+
+
+ Rapports personnalisés
+
+
+
+
+
+ Support prioritaire
+
+
+
+
+
+ API disponible
+
+
+
+ Commencer
+
+
+
+
+
+
+
+
+
+ >
+ }
+ />
+ } />
+ } />
+
+
+
+
);
};
diff --git a/ui/src/pages/login.tsx b/ui/src/pages/login.tsx
new file mode 100644
index 0000000..96db704
--- /dev/null
+++ b/ui/src/pages/login.tsx
@@ -0,0 +1,157 @@
+import { Button } from "../ui-components/button";
+import { twMerge } from "tailwind-merge";
+
+export function LoginPage() {
+ return (
+
+
+
+ Connexion
+
+
+
+
+
+
+
+
+
+
+ Continuer avec Google
+
+
+
+
+
+
+ Continuer avec Apple
+
+
+
+
+
+
+ Ou continuer avec
+
+
+
+
+
+
+
+ Pas encore de compte ?{" "}
+
+ S'inscrire
+
+
+
+
+
+ );
+}
diff --git a/ui/src/pages/signup.tsx b/ui/src/pages/signup.tsx
new file mode 100644
index 0000000..5d66699
--- /dev/null
+++ b/ui/src/pages/signup.tsx
@@ -0,0 +1,229 @@
+import { Button } from "../ui-components/button";
+import { twMerge } from "tailwind-merge";
+
+export function SignUpPage() {
+ return (
+
+
+
+ Créer un compte
+
+
+
+
+
+
+
+
+
+
+ Continuer avec Google
+
+
+
+
+
+
+ Continuer avec Apple
+
+
+
+
+
+
+ Ou continuer avec
+
+
+
+
+
+
+
+ Déjà un compte ?{" "}
+
+ Se connecter
+
+
+
+
+
+ );
+}
diff --git a/ui/src/ui-components/header.tsx b/ui/src/ui-components/header.tsx
index c83f0b5..c8ffb6c 100644
--- a/ui/src/ui-components/header.tsx
+++ b/ui/src/ui-components/header.tsx
@@ -5,6 +5,7 @@ import logo from "../assets/icon.jpg";
import { useState } from "react";
import { ConnectionForm } from "./connection-form";
import { SignUpForm } from "./signup-form";
+import { Link, useNavigate } from "react-router-dom";
interface HeaderProps {
theme: "dark" | "light" | "system";
@@ -14,24 +15,25 @@ interface HeaderProps {
export function Header({ theme, onThemeChange }: HeaderProps) {
const [showConnectionForm, setShowConnectionForm] = useState(false);
const [showSignUpForm, setShowSignUpForm] = useState(false);
+ const navigate = useNavigate();
return (
<>
-
+
-
+
XTablo
-
+
@@ -40,7 +42,7 @@ export function Header({ theme, onThemeChange }: HeaderProps) {
@@ -49,7 +51,7 @@ export function Header({ theme, onThemeChange }: HeaderProps) {
@@ -59,19 +61,19 @@ export function Header({ theme, onThemeChange }: HeaderProps) {
setShowConnectionForm(true)}
+ onPress={() => navigate("/login")}
className={twMerge(
- "text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-white/80",
- "border-slate-300 dark:border-white/30 hover:border-slate-400 dark:hover:border-white/50",
+ "text-slate-900 dark:text-white hover:text-emerald-700 dark:hover:text-emerald-400",
+ "border-emerald-300 dark:border-emerald-700/30 hover:border-emerald-400 dark:hover:border-emerald-600/50",
"transition"
)}
>
Connexion
setShowSignUpForm(true)}
+ onPress={() => navigate("/signup")}
className={twMerge(
- "bg-blue-950 text-white hover:bg-blue-900",
+ "bg-emerald-700 text-white hover:bg-emerald-600",
"transition-colors"
)}
>
diff --git a/ui/src/ui-components/switch.tsx b/ui/src/ui-components/switch.tsx
index efcbfec..4d72321 100644
--- a/ui/src/ui-components/switch.tsx
+++ b/ui/src/ui-components/switch.tsx
@@ -1,5 +1,5 @@
-import React from 'react';
-import { twMerge } from 'tailwind-merge';
+import React from "react";
+import { twMerge } from "tailwind-merge";
import {
composeRenderProps,
Group,
@@ -7,9 +7,9 @@ import {
Switch as RACSwitch,
SwitchProps as RACSwitchProps,
SwitchRenderProps,
-} from 'react-aria-components';
-import { groupBox, composeTailwindRenderProps } from './utils';
-import { DescriptionProvider, DescriptionContext, LabeledGroup } from './field';
+} from "react-aria-components";
+import { groupBox, composeTailwindRenderProps } from "./utils";
+import { DescriptionProvider, DescriptionContext, LabeledGroup } from "./field";
export function SwitchGroup(props: GroupProps) {
return (
@@ -25,16 +25,16 @@ export function SwitchGroup(props: GroupProps) {
export function Switches({
className,
...props
-}: React.JSX.IntrinsicElements['div']) {
+}: React.JSX.IntrinsicElements["div"]) {
return (
);
@@ -43,19 +43,19 @@ export function Switches({
export function SwitchField({
className,
...props
-}: React.JSX.IntrinsicElements['div']) {
+}: React.JSX.IntrinsicElements["div"]) {
return (
@@ -63,13 +63,13 @@ export function SwitchField({
}
interface SwitchProps extends RACSwitchProps {
- labelPlacement?: 'start' | 'end';
- size?: 'lg';
+ labelPlacement?: "start" | "end";
+ size?: "lg";
render?: never;
}
export interface CustomRenderSwitchProps
- extends Omit {
+ extends Omit {
render: React.ReactElement | ((props: SwitchRenderProps) => React.ReactNode);
children?: never;
size?: never;
@@ -85,15 +85,15 @@ export function Switch(props: SwitchProps | CustomRenderSwitchProps) {
return (
twMerge(
- 'group text-base/6 sm:text-sm/6',
- isDisabled && 'opacity-50',
- className,
- ),
+ "group text-base/6 sm:text-sm/6",
+ isDisabled && "opacity-50",
+ className
+ )
)}
>
{render}
@@ -101,53 +101,53 @@ export function Switch(props: SwitchProps | CustomRenderSwitchProps) {
);
}
- const { labelPlacement = 'end', size, children, ...restProps } = props;
+ const { labelPlacement = "end", size, children, ...restProps } = props;
return (
twMerge(
- 'group flex items-center text-base/6 sm:text-sm/6',
- labelPlacement === 'start' && 'flex-row-reverse justify-between',
- isDisabled && 'opacity-50',
- className,
- ),
+ "group flex items-center text-base/6 sm:text-sm/6",
+ labelPlacement === "start" && "flex-row-reverse justify-between",
+ isDisabled && "opacity-50",
+ className
+ )
)}
>
{(renderProps) => (
<>
- {typeof children === 'function' ? children(renderProps) : children}
+ {typeof children === "function" ? children(renderProps) : children}
>
)}
diff --git a/ui/stats.html b/ui/stats.html
index 28f1ff7..fdb818a 100644
--- a/ui/stats.html
+++ b/ui/stats.html
@@ -4929,7 +4929,7 @@ var drawChart = (function (exports) {