Add login and signup form

This commit is contained in:
Arthur Belleville 2025-03-17 21:55:15 +01:00
parent f62175e490
commit 0bb4963897
No known key found for this signature in database
8 changed files with 943 additions and 452 deletions

View file

@ -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"
}
}

View file

@ -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

View file

@ -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 (
<div
className={twMerge(
"min-h-screen bg-gradient-to-br from-emerald-100 via-green-100 to-white",
"dark:bg-gradient-to-br dark:from-[#0a1f0a] dark:via-[#051505] dark:to-black"
)}
>
<Header theme={theme} onThemeChange={setTheme} />
<div className="container mx-auto px-4 py-16">
<main className="text-center">
<h2 className="text-5xl font-bold text-slate-900 dark:text-white mb-6">
Maîtrisez vos dépenses de chantier
</h2>
<p className="text-xl text-slate-700 dark:text-white mb-12 max-w-2xl mx-auto">
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.
</p>
<Button
className={twMerge(
"inline-flex items-center gap-2 bg-blue-950 px-8 py-4 rounded-full",
"text-white font-semibold hover:bg-blue-900 transition-colors",
"shadow-lg hover:shadow-xl shadow-blue-950/20"
)}
>
Démarrer gratuitement
</Button>
<div className="mt-20">
<div
className={twMerge(
"bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl p-8",
"shadow-xl border border-emerald-200 dark:border-slate-700/50"
)}
>
<div className="aspect-video rounded-lg bg-emerald-100 dark:bg-slate-700/50 animate-pulse" />
</div>
</div>
<section id="features" className="mt-24">
<h3 className="text-3xl font-bold text-slate-900 dark:text-white mb-12">
Fait confiance par les entreprises du BTP
</h3>
<div className="flex overflow-x-hidden gap-12 pb-4 -mx-4 px-4 scrollbar-hide">
<div
className={twMerge(
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
)}
>
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
"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."
</p>
<div className="flex items-center mt-auto">
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
<div className="ml-3">
<p className="text-slate-900 dark:text-white font-medium">
Michel Dubois
</p>
<p className="text-slate-600 dark:text-white/80 text-sm">
Chef de Projet
</p>
</div>
</div>
</div>
<div
className={twMerge(
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
)}
>
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
"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."
</p>
<div className="flex items-center mt-auto">
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
<div className="ml-3">
<p className="text-slate-900 dark:text-white font-medium">
Sophie Martin
</p>
<p className="text-slate-600 dark:text-white/80 text-sm">
Directrice Financière
</p>
</div>
</div>
</div>
<div
className={twMerge(
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
)}
>
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
"La gestion des dépenses sur plusieurs chantiers n'a jamais
é aussi simple. XTablo nous donne une visibilité totale sur
nos coûts."
</p>
<div className="flex items-center mt-auto">
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
<div className="ml-3">
<p className="text-slate-900 dark:text-white font-medium">
David Laurent
</p>
<p className="text-slate-600 dark:text-white/80 text-sm">
Chef de Chantier
</p>
</div>
</div>
</div>
</div>
</section>
<section id="pricing" className="mt-24">
<h3 className="text-3xl font-bold text-slate-900 dark:text-white mb-12">
Des tarifs adaptés à vos besoins
</h3>
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<div
className={twMerge(
"bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl p-8",
"border border-emerald-200 dark:border-white/30 hover:border-emerald-300 dark:hover:border-white/50",
"transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl",
"hover:shadow-emerald-200/20 dark:hover:shadow-emerald-900/20 flex flex-col"
)}
>
<h4 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">
Starter
</h4>
<div className="mb-6">
<span className="text-4xl font-bold text-slate-900 dark:text-white">
12
</span>
<span className="text-slate-600 dark:text-white/80">
/mois
</span>
</div>
<ul className="space-y-4 mb-8 flex-grow">
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Jusqu'à 5 chantiers
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Suivi des dépenses en temps réel
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Rapports mensuels
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Support par email
</li>
</ul>
<button
className={twMerge(
"w-full px-6 py-3 bg-blue-950 rounded-full text-white font-semibold",
"hover:bg-blue-900 transition-all duration-300 hover:scale-[1.02]",
"hover:shadow-lg hover:shadow-xl shadow-blue-950/20 mt-auto"
)}
>
Commencer
</button>
</div>
<div
className={twMerge(
"bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl p-8",
"border border-emerald-200 dark:border-white/50 relative flex flex-col",
"transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl",
"hover:shadow-emerald-200/20 dark:hover:shadow-emerald-900/20"
)}
>
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 transition-all duration-300 group-hover:scale-110">
<span className="bg-blue-950 text-white px-4 py-1 rounded-full text-sm font-semibold">
Plus populaire
</span>
</div>
<h4 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">
Pro
</h4>
<div className="mb-6">
<span className="text-4xl font-bold text-slate-900 dark:text-white">
25
</span>
<span className="text-slate-600 dark:text-white/80">
/mois
</span>
</div>
<ul className="space-y-4 mb-8 flex-grow">
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Chantiers illimités
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Suivi des dépenses en temps réel
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Rapports personnalisés
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Support prioritaire
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
API disponible
</li>
</ul>
<button
className={twMerge(
"w-full px-6 py-3 bg-blue-950 rounded-full text-white font-semibold",
"hover:bg-blue-900 transition-all duration-300 hover:scale-[1.02]",
"hover:shadow-lg hover:shadow-xl shadow-blue-950/20 mt-auto"
)}
>
Commencer
</button>
</div>
</div>
</section>
<section id="contact" className="mt-24 text-center">
<h3 className="text-3xl font-bold text-slate-900 dark:text-white mb-6">
Prêt à optimiser vos dépenses de projet ?
</h3>
<p className="text-xl text-slate-700 dark:text-white mb-12 max-w-2xl mx-auto">
Commencez votre essai gratuit de 14 jours aujourd'hui. Aucune
carte bancaire requise.
</p>
<div className="flex justify-center gap-6">
<button
className={twMerge(
"px-8 py-3 bg-blue-950 rounded-full text-white font-semibold",
"hover:bg-blue-900 transition shadow-lg hover:shadow-xl shadow-blue-950/20"
)}
>
Essai gratuit
</button>
<button
className={twMerge(
"px-8 py-3 border border-blue-400/30 rounded-full",
"text-slate-900 dark:text-white font-semibold hover:bg-blue-950/30 transition"
)}
>
Contacter les ventes
</button>
</div>
</section>
</main>
</div>
<footer
<Router>
<div
className={twMerge(
"mt-24 py-8 border-t border-slate-200 dark:border-slate-800"
"min-h-screen bg-gradient-to-br from-emerald-100 via-green-100 to-white",
"dark:bg-gradient-to-br dark:from-[#0a1f0a] dark:via-[#051505] dark:to-black"
)}
>
<div className="container mx-auto px-4 text-center">
<p className="text-slate-600 dark:text-slate-400 text-sm">
© {new Date().getFullYear()} XTablo. Tous droits réservés.
</p>
<p className="text-slate-500 dark:text-slate-500 text-xs mt-2">
XTablo est une marque déposée. Les logos et noms de marques sont des
marques déposées de leurs propriétaires respectifs.
</p>
</div>
</footer>
<style>
{`
@keyframes slide {
0% { transform: translateX(-100vw); }
100% { transform: translateX(100vw); }
}
.animate-slide {
animation: slide 24s linear infinite;
}
`}
</style>
</div>
<Routes>
<Route
path="/"
element={
<>
<Header theme={theme} onThemeChange={setTheme} />
<div className="container mx-auto px-4 py-16">
<main className="text-center">
<h2 className="text-5xl font-bold text-slate-900 dark:text-white mb-6">
Maîtrisez vos dépenses de chantier
</h2>
<p className="text-xl text-slate-700 dark:text-white mb-12 max-w-2xl mx-auto">
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.
</p>
<Button
className={twMerge(
"inline-flex items-center gap-2 bg-emerald-700 px-8 py-4 rounded-full",
"text-white font-semibold hover:bg-emerald-600 transition-colors",
"shadow-lg hover:shadow-xl shadow-emerald-700/20"
)}
>
Démarrer gratuitement
</Button>
<div className="mt-20">
<div
className={twMerge(
"bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl p-8",
"shadow-xl border border-emerald-200 dark:border-slate-700/50"
)}
>
<div className="aspect-video rounded-lg bg-emerald-100 dark:bg-slate-700/50 animate-pulse" />
</div>
</div>
<section id="features" className="mt-24">
<h3 className="text-3xl font-bold text-slate-900 dark:text-white mb-12">
Fait confiance par les entreprises du BTP
</h3>
<div className="flex overflow-x-hidden gap-12 pb-4 -mx-4 px-4 scrollbar-hide">
<div
className={twMerge(
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
)}
>
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
"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."
</p>
<div className="flex items-center mt-auto">
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
<div className="ml-3">
<p className="text-slate-900 dark:text-white font-medium">
Michel Dubois
</p>
<p className="text-slate-600 dark:text-white/80 text-sm">
Chef de Projet
</p>
</div>
</div>
</div>
<div
className={twMerge(
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
)}
>
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
"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."
</p>
<div className="flex items-center mt-auto">
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
<div className="ml-3">
<p className="text-slate-900 dark:text-white font-medium">
Sophie Martin
</p>
<p className="text-slate-600 dark:text-white/80 text-sm">
Directrice Financière
</p>
</div>
</div>
</div>
<div
className={twMerge(
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
)}
>
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
"La gestion des dépenses sur plusieurs chantiers n'a
jamais é aussi simple. XTablo nous donne une
visibilité totale sur nos coûts."
</p>
<div className="flex items-center mt-auto">
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
<div className="ml-3">
<p className="text-slate-900 dark:text-white font-medium">
David Laurent
</p>
<p className="text-slate-600 dark:text-white/80 text-sm">
Chef de Chantier
</p>
</div>
</div>
</div>
</div>
</section>
<section id="pricing" className="mt-24">
<h3 className="text-3xl font-bold text-slate-900 dark:text-white mb-12">
Des tarifs adaptés à vos besoins
</h3>
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<div
className={twMerge(
"bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl p-8",
"border border-emerald-200 dark:border-white/30 hover:border-emerald-300 dark:hover:border-white/50",
"transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl",
"hover:shadow-emerald-200/20 dark:hover:shadow-emerald-900/20 flex flex-col"
)}
>
<h4 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">
Starter
</h4>
<div className="mb-6">
<span className="text-4xl font-bold text-slate-900 dark:text-white">
12
</span>
<span className="text-slate-600 dark:text-white/80">
/mois
</span>
</div>
<ul className="space-y-4 mb-8 flex-grow">
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Jusqu'à 5 chantiers
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Suivi des dépenses en temps réel
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Rapports mensuels
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Support par email
</li>
</ul>
<button
className={twMerge(
"w-full px-6 py-3 bg-emerald-700 rounded-full text-white font-semibold",
"hover:bg-emerald-600 transition-all duration-300 hover:scale-[1.02]",
"hover:shadow-lg hover:shadow-xl shadow-emerald-700/20 mt-auto"
)}
>
Commencer
</button>
</div>
<div
className={twMerge(
"bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl p-8",
"border border-emerald-200 dark:border-white/50 relative flex flex-col",
"transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl",
"hover:shadow-emerald-200/20 dark:hover:shadow-emerald-900/20"
)}
>
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 transition-all duration-300 group-hover:scale-110">
<span className="bg-emerald-700 text-white px-4 py-1 rounded-full text-sm font-semibold">
Plus populaire
</span>
</div>
<h4 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">
Pro
</h4>
<div className="mb-6">
<span className="text-4xl font-bold text-slate-900 dark:text-white">
25
</span>
<span className="text-slate-600 dark:text-white/80">
/mois
</span>
</div>
<ul className="space-y-4 mb-8 flex-grow">
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Chantiers illimités
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Suivi des dépenses en temps réel
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Rapports personnalisés
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
Support prioritaire
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-slate-700 dark:text-white mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
API disponible
</li>
</ul>
<button
className={twMerge(
"w-full px-6 py-3 bg-emerald-700 rounded-full text-white font-semibold",
"hover:bg-emerald-600 transition-all duration-300 hover:scale-[1.02]",
"hover:shadow-lg hover:shadow-xl shadow-emerald-700/20 mt-auto"
)}
>
Commencer
</button>
</div>
</div>
</section>
<section id="contact" className="mt-24 text-center">
<h3 className="text-3xl font-bold text-slate-900 dark:text-white mb-6">
Prêt à optimiser vos dépenses de projet ?
</h3>
<p className="text-xl text-slate-700 dark:text-white mb-12 max-w-2xl mx-auto">
Commencez votre essai gratuit de 14 jours aujourd'hui.
Aucune carte bancaire requise.
</p>
<div className="flex justify-center gap-6">
<button
className={twMerge(
"px-8 py-3 bg-emerald-700 rounded-full text-white font-semibold",
"hover:bg-emerald-600 transition shadow-lg hover:shadow-xl shadow-emerald-700/20"
)}
>
Essai gratuit
</button>
<button
className={twMerge(
"px-8 py-3 border border-emerald-400/30 rounded-full",
"text-slate-900 dark:text-white font-semibold hover:bg-emerald-700/30 transition"
)}
>
Contacter les ventes
</button>
</div>
</section>
</main>
</div>
<footer
className={twMerge(
"mt-24 py-8 border-t border-slate-200 dark:border-slate-800"
)}
>
<div className="container mx-auto px-4 text-center">
<p className="text-slate-600 dark:text-slate-400 text-sm">
© {new Date().getFullYear()} XTablo. Tous droits réservés.
</p>
<p className="text-slate-500 dark:text-slate-500 text-xs mt-2">
XTablo est une marque déposée. Les logos et noms de
marques sont des marques déposées de leurs propriétaires
respectifs.
</p>
</div>
</footer>
</>
}
/>
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignUpPage />} />
</Routes>
<style>
{`
@keyframes slide {
0% { transform: translateX(-100vw); }
100% { transform: translateX(100vw); }
}
.animate-slide {
animation: slide 24s linear infinite;
}
`}
</style>
</div>
</Router>
);
};

157
ui/src/pages/login.tsx Normal file
View file

@ -0,0 +1,157 @@
import { Button } from "../ui-components/button";
import { twMerge } from "tailwind-merge";
export function LoginPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-emerald-100 via-green-100 to-white dark:bg-gradient-to-br dark:from-[#0a1f0a] dark:via-[#051505] dark:to-black">
<div
className={twMerge(
"w-full max-w-md p-8 bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl",
"border border-emerald-200 dark:border-emerald-900/30",
"shadow-xl"
)}
>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-8 text-center">
Connexion
</h1>
<div className="space-y-4">
<Button
className={twMerge(
"w-full bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"border border-slate-200 dark:border-slate-700",
"hover:bg-slate-50 dark:hover:bg-slate-700/50",
"flex items-center justify-center gap-2"
)}
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Continuer avec Google
</Button>
<Button
className={twMerge(
"w-full bg-black text-white",
"hover:bg-slate-900",
"flex items-center justify-center gap-2"
)}
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 2C6.477 2 2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.879V14.89h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.989C18.343 21.129 22 16.99 22 12c0-5.523-4.477-10-10-10z"
/>
</svg>
Continuer avec Apple
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200 dark:border-slate-700"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-slate-800 text-slate-500">
Ou continuer avec
</span>
</div>
</div>
<form className="space-y-4">
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
>
Email
</label>
<input
type="email"
id="email"
className={twMerge(
"w-full px-4 py-2 rounded-lg border border-slate-200 dark:border-slate-700",
"bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
)}
/>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
>
Mot de passe
</label>
<input
type="password"
id="password"
className={twMerge(
"w-full px-4 py-2 rounded-lg border border-slate-200 dark:border-slate-700",
"bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
)}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
type="checkbox"
id="remember"
className="h-4 w-4 text-emerald-600 focus:ring-emerald-500 border-slate-300 rounded"
/>
<label
htmlFor="remember"
className="ml-2 block text-sm text-slate-700 dark:text-slate-300"
>
Se souvenir de moi
</label>
</div>
<a
href="#"
className="text-sm text-emerald-600 hover:text-emerald-500"
>
Mot de passe oublié ?
</a>
</div>
<Button
className={twMerge(
"w-full bg-emerald-700 text-white",
"hover:bg-emerald-600"
)}
>
Se connecter
</Button>
</form>
<p className="mt-6 text-center text-sm text-slate-600 dark:text-slate-400">
Pas encore de compte ?{" "}
<a
href="/signup"
className="text-emerald-600 hover:text-emerald-500 font-medium"
>
S'inscrire
</a>
</p>
</div>
</div>
</div>
);
}

229
ui/src/pages/signup.tsx Normal file
View file

@ -0,0 +1,229 @@
import { Button } from "../ui-components/button";
import { twMerge } from "tailwind-merge";
export function SignUpPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-emerald-100 via-green-100 to-white dark:bg-gradient-to-br dark:from-[#0a1f0a] dark:via-[#051505] dark:to-black">
<div
className={twMerge(
"w-full max-w-md p-8 bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl",
"border border-emerald-200 dark:border-emerald-900/30",
"shadow-xl"
)}
>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-8 text-center">
Créer un compte
</h1>
<div className="space-y-4">
<Button
className={twMerge(
"w-full bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"border border-slate-200 dark:border-slate-700",
"hover:bg-slate-50 dark:hover:bg-slate-700/50",
"flex items-center justify-center gap-2"
)}
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Continuer avec Google
</Button>
<Button
className={twMerge(
"w-full bg-black text-white",
"hover:bg-slate-900",
"flex items-center justify-center gap-2"
)}
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 2C6.477 2 2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.879V14.89h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.989C18.343 21.129 22 16.99 22 12c0-5.523-4.477-10-10-10z"
/>
</svg>
Continuer avec Apple
</Button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200 dark:border-slate-700"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-slate-800 text-slate-500">
Ou continuer avec
</span>
</div>
</div>
<form className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label
htmlFor="firstName"
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
>
Prénom
</label>
<input
type="text"
id="firstName"
className={twMerge(
"w-full px-4 py-2 rounded-lg border border-slate-200 dark:border-slate-700",
"bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
)}
/>
</div>
<div>
<label
htmlFor="lastName"
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
>
Nom
</label>
<input
type="text"
id="lastName"
className={twMerge(
"w-full px-4 py-2 rounded-lg border border-slate-200 dark:border-slate-700",
"bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
)}
/>
</div>
</div>
<div>
<label
htmlFor="company"
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
>
Nom de l'entreprise
</label>
<input
type="text"
id="company"
className={twMerge(
"w-full px-4 py-2 rounded-lg border border-slate-200 dark:border-slate-700",
"bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
)}
/>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
>
Email professionnel
</label>
<input
type="email"
id="email"
className={twMerge(
"w-full px-4 py-2 rounded-lg border border-slate-200 dark:border-slate-700",
"bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
)}
/>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
>
Mot de passe
</label>
<input
type="password"
id="password"
className={twMerge(
"w-full px-4 py-2 rounded-lg border border-slate-200 dark:border-slate-700",
"bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
)}
/>
</div>
<div>
<label
htmlFor="confirmPassword"
className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"
>
Confirmer le mot de passe
</label>
<input
type="password"
id="confirmPassword"
className={twMerge(
"w-full px-4 py-2 rounded-lg border border-slate-200 dark:border-slate-700",
"bg-white dark:bg-slate-800 text-slate-900 dark:text-white",
"focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
)}
/>
</div>
<div className="flex items-start">
<input
type="checkbox"
id="terms"
className="mt-1 h-4 w-4 text-emerald-600 focus:ring-emerald-500 border-slate-300 rounded"
/>
<label
htmlFor="terms"
className="ml-2 block text-sm text-slate-700 dark:text-slate-300"
>
J'accepte les{" "}
<a href="#" className="text-emerald-600 hover:text-emerald-500">
conditions d'utilisation
</a>{" "}
et la{" "}
<a href="#" className="text-emerald-600 hover:text-emerald-500">
politique de confidentialité
</a>
</label>
</div>
<Button
className={twMerge(
"w-full bg-emerald-700 text-white",
"hover:bg-emerald-600"
)}
>
Créer mon compte
</Button>
</form>
<p className="mt-6 text-center text-sm text-slate-600 dark:text-slate-400">
Déjà un compte ?{" "}
<a
href="/login"
className="text-emerald-600 hover:text-emerald-500 font-medium"
>
Se connecter
</a>
</p>
</div>
</div>
</div>
);
}

View file

@ -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 (
<>
<header className="sticky top-0 z-50 w-full border-b border-slate-200 dark:border-slate-800 bg-white/80 dark:bg-slate-900/80 backdrop-blur-lg">
<header className="sticky top-0 z-50 w-full border-b border-emerald-200 dark:border-emerald-900/30 bg-emerald-100/90 dark:bg-[#0a1f0a]/90 backdrop-blur-lg">
<div className="container mx-auto px-4">
<nav className="flex items-center justify-between h-16">
<div className="flex items-center gap-2">
<Link to="/" className="flex items-center gap-2">
<img src={logo} alt="Logo XTablo" className="w-8 h-8" />
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
XTablo
</h1>
</div>
</Link>
<div className="flex items-center gap-6">
<div className="hidden md:flex items-center gap-6">
<a
href="#features"
className={twMerge(
"text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white",
"text-slate-600 dark:text-slate-300 hover:text-emerald-700 dark:hover:text-emerald-400",
"transition-colors"
)}
>
@ -40,7 +42,7 @@ export function Header({ theme, onThemeChange }: HeaderProps) {
<a
href="#pricing"
className={twMerge(
"text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white",
"text-slate-600 dark:text-slate-300 hover:text-emerald-700 dark:hover:text-emerald-400",
"transition-colors"
)}
>
@ -49,7 +51,7 @@ export function Header({ theme, onThemeChange }: HeaderProps) {
<a
href="#contact"
className={twMerge(
"text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white",
"text-slate-600 dark:text-slate-300 hover:text-emerald-700 dark:hover:text-emerald-400",
"transition-colors"
)}
>
@ -59,19 +61,19 @@ export function Header({ theme, onThemeChange }: HeaderProps) {
<div className="flex items-center gap-4">
<Button
variant="outline"
onPress={() => 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
</Button>
<Button
onPress={() => 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"
)}
>

View file

@ -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 (
<div
{...props}
data-ui="box"
className={twMerge(
'flex flex-col',
"flex flex-col",
// When any switch item has description, apply all `font-medium` to all switch item labels
'has-data-[ui=description]:[&_label]:font-medium',
className,
"has-data-[ui=description]:[&_label]:font-medium",
className
)}
/>
);
@ -43,19 +43,19 @@ export function Switches({
export function SwitchField({
className,
...props
}: React.JSX.IntrinsicElements['div']) {
}: React.JSX.IntrinsicElements["div"]) {
return (
<DescriptionProvider>
<div
{...props}
data-ui="field"
className={twMerge(
'group flex flex-col gap-y-1',
'has-[label[data-label-placement=start]]:[&_[data-ui=description]:not([class*=pe-])]:pe-[calc(theme(width.8)+16px)]',
'has-[label[data-label-placement=end]]:[&_[data-ui=description]:not([class*=ps-])]:ps-[calc(theme(width.8)+12px)]',
'has-data-[ui=description]:[&_label]:font-medium',
'has-[label[data-disabled]]:**:data-[ui=description]:opacity-50',
className,
"group flex flex-col gap-y-1",
"has-[label[data-label-placement=start]]:[&_[data-ui=description]:not([class*=pe-])]:pe-[calc(theme(width.8)+16px)]",
"has-[label[data-label-placement=end]]:[&_[data-ui=description]:not([class*=ps-])]:ps-[calc(theme(width.8)+12px)]",
"has-data-[ui=description]:[&_label]:font-medium",
"has-[label[data-disabled]]:**:data-[ui=description]:opacity-50",
className
)}
/>
</DescriptionProvider>
@ -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<RACSwitchProps, 'children'> {
extends Omit<RACSwitchProps, "children"> {
render: React.ReactElement | ((props: SwitchRenderProps) => React.ReactNode);
children?: never;
size?: never;
@ -85,15 +85,15 @@ export function Switch(props: SwitchProps | CustomRenderSwitchProps) {
return (
<RACSwitch
{...restProps}
aria-describedby={descriptionContext?.['aria-describedby']}
aria-describedby={descriptionContext?.["aria-describedby"]}
className={composeRenderProps(
props.className,
(className, { isDisabled }) =>
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 (
<RACSwitch
{...restProps}
aria-describedby={descriptionContext?.['aria-describedby']}
aria-describedby={descriptionContext?.["aria-describedby"]}
data-label-placement={labelPlacement}
className={composeRenderProps(
props.className,
(className, { isDisabled }) =>
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) => (
<>
<div
className={twMerge(
'dark:border-input flex h-6 w-11 shrink-0 cursor-default items-center rounded-full border border-zinc-200 bg-zinc-200 p-px dark:bg-transparent',
size !== 'lg' && 'sm:h-5 sm:w-8',
labelPlacement === 'end' ? 'me-3' : 'ms-3',
renderProps.isReadOnly && 'opacity-50',
"dark:border-input flex h-6 w-11 shrink-0 cursor-default items-center rounded-full border border-zinc-200 bg-zinc-200 p-px dark:bg-transparent",
size !== "lg" && "sm:h-5 sm:w-8",
labelPlacement === "end" ? "me-3" : "ms-3",
renderProps.isReadOnly && "opacity-50",
renderProps.isSelected &&
'border-accent bg-accent dark:bg-accent dark:border-accent',
renderProps.isDisabled && 'bg-gray-200 dark:bg-zinc-700',
"border-accent bg-accent dark:bg-accent dark:border-accent",
renderProps.isDisabled && "bg-gray-200 dark:bg-zinc-700",
renderProps.isFocusVisible &&
'outline-ring outline outline-2 outline-offset-2',
"outline-ring outline outline-offset-2"
)}
>
<span
data-ui="handle"
className={twMerge(
'size-5',
size !== 'lg' && 'sm:size-4',
'rounded-full bg-white shadow transition-all ease-in-out',
"size-5",
size !== "lg" && "sm:size-4",
"rounded-full bg-white shadow transition-all ease-in-out",
renderProps.isSelected && [
'translate-x-5 bg-[lch(from_var(--color-accent)_calc((49.44_-_l)_*_infinity)_0_0)] rtl:-translate-x-5',
size !== 'lg' && 'sm:translate-x-3 sm:rtl:-translate-x-3',
],
"translate-x-5 bg-[lch(from_var(--color-accent)_calc((49.44_-_l)_*_infinity)_0_0)] rtl:-translate-x-5",
size !== "lg" && "sm:translate-x-3 sm:rtl:-translate-x-3",
]
)}
/>
</div>
{typeof children === 'function' ? children(renderProps) : children}
{typeof children === "function" ? children(renderProps) : children}
</>
)}
</RACSwitch>

File diff suppressed because one or more lines are too long