Add reset-password and improve login/signup

This commit is contained in:
Arthur Belleville 2025-03-23 10:57:30 +01:00
parent 29e7741ff5
commit 707d9aa5ac
No known key found for this signature in database
19 changed files with 1383 additions and 783 deletions

Binary file not shown.

Binary file not shown.

46
backend/app/auth.py Normal file
View file

@ -0,0 +1,46 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from supabase import Client
from .config import settings
from jose import JWTError, jwt
import os
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
def get_supabase() -> Client:
from supabase import create_client
# Temporary hardcoded values
import os
# Access environment variables
# Debugging purpose
url = settings.supabase_url
key = settings.supabase_key # From Supabase dashboard
# print("[HARDCODED] URL:", url)
# print("[HARDCODED] Key:", key[:10] + "...")
return create_client(url, key)
# Updated current user dependency
async def get_current_user(
token: str = Depends(oauth2_scheme),
supabase: Client = Depends(get_supabase)
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# Get user from Supabase auth
user = supabase.auth.get_user(token)
if not user:
raise credentials_exception
return user
except Exception as e:
raise credentials_exception

16
backend/app/config.py Normal file
View file

@ -0,0 +1,16 @@
from pydantic_settings import BaseSettings
from pathlib import Path
from dotenv import load_dotenv
import os
# Load environment variables from the .env file
env_path = Path(__file__).parent / ".env"
load_dotenv(dotenv_path=env_path)
class Settings(BaseSettings):
supabase_url: str = os.getenv("SUPABASE_URL")
supabase_key: str = os.getenv("SUPABASE_KEY")
secret_key: str = os.getenv("SECRET_KEY")
access_token_expire_minutes: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 30))
settings = Settings()

View file

@ -1,54 +1,103 @@
import os
import json
import random
from typing import Dict, List, Optional
import inspect
from typing import Annotated, Dict, List, Optional
from contextlib import contextmanager
from fastapi import FastAPI, Depends, HTTPException, status
from .auth import get_supabase
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from pydantic import BaseModel, EmailStr, field_validator, ValidationInfo, Field, SecretStr
from pydantic_core.core_schema import FieldValidationInfo
from dotenv import load_dotenv
from supabase import create_client, Client
from supabase import Client
from .auth import get_current_user
from uuid import uuid4
from datetime import datetime
# Load environment variables
load_dotenv()
# Initialize Supabase client
supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
supabase: Client = create_client(supabase_url, supabase_key)
# Initialize FastAPI app
app = FastAPI(title="XTablo API")
# CORS Middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Error-Code", "X-Error-Message"]
)
# Security
security = HTTPBearer()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = exc.errors()
custom_errors = []
for error in errors:
custom_message = error["msg"]
if custom_message.startswith("Value error, "):
custom_message = custom_message.split(", ")[1]
custom_errors.append({
**error,
"form_location": error["loc"][-1],
"human_error": custom_message
})
return JSONResponse(status_code=422, content=jsonable_encoder(custom_errors))
# ======================================
# 🚀 MODELS
# ======================================
class UserCreate(BaseModel):
email: str
email: EmailStr
first_name: str
last_name: str
password: str
username: str
confirm_password: str
business_name: str
@field_validator("email")
def email_must_contain_at_symbol(cls, v):
if '@' not in v:
raise ValueError("Entrer un email valide")
return v
@field_validator("password")
def password_must_contain_at_least_8_characters(cls, v):
if len(v) < 8:
raise ValueError("Le mot de passe doit contenir au moins 8 caractères")
return v
@field_validator("business_name")
def business_name_must_contain_at_least_3_characters(cls, v):
if len(v) < 3:
raise ValueError("Le nom de la société doit contenir au moins 3 caractères")
return v
@field_validator('confirm_password', mode='before')
def passwords_match(cls, v, info: FieldValidationInfo):
if 'password' in info.data and v != info.data['password']:
raise ValueError('Les mots de passe ne correspondent pas')
return v
class UserLogin(BaseModel):
email: str
email: EmailStr
password: str
class UserOut(BaseModel):
email: EmailStr
business_name: str
class GameState(BaseModel):
id: str
@ -99,63 +148,101 @@ class RefreshResponse(BaseModel):
expires_at: int
user: dict
# ======================================
# 🔒 AUTHENTICATION HELPERS
# ======================================
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
try:
response = supabase.auth.get_user(token)
if not hasattr(response, "user") or not response.user:
raise Exception("Invalid user data from Supabase")
class UserResponse:
def __init__(self, id, email, username):
self.id = id
self.email = email
self.username = username
# Extract username from user metadata or profile
username = response.user.user_metadata.get("username", "Unknown") # Adjust the key as needed
# Pass id, email, and username to UserResponse
return UserResponse(response.user.id, response.user.email, username)
except Exception:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials")
# ======================================
# 🔥 AUTH ROUTES
# ======================================
@app.post("/auth/register")
async def register(user: UserCreate):
async def register(user: UserCreate, supabase: Client = Depends(get_supabase)):
try:
return supabase.auth.sign_up({
"email": user.email,
"password": user.password,
"options": {"data": {"username": user.username, "first_name": user.first_name, "last_name": user.last_name}}
"options": {"data": {"first_name": user.first_name, "last_name": user.last_name, "business_name": user.business_name}}
})
except Exception as e:
headers = {}
if e.code == "user_already_exists":
headers = {"X-Error-Code": e.code, "X-Error-Message": "Cette adresse email est déjà utilisée"}
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@app.post("/auth/login")
async def login(user: UserLogin):
async def login(credentials: UserLogin, supabase: Client = Depends(get_supabase)):
try:
return supabase.auth.sign_in_with_password({"email": user.email, "password": user.password})
print("Login attempt for:", credentials.email) # Debug log
response = supabase.auth.sign_in_with_password({
"email": credentials.email.strip(),
"password": credentials.password.strip()
})
print("Login response:", response)
return {
"access_token": response.session.access_token,
"token_type": "bearer"
}
except Exception as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
headers = {}
if e.code == "invalid_credentials":
headers = {"X-Error-Code": e.code, "X-Error-Message": "Email ou mot de passe incorrect"}
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers=headers
)
@app.get("/auth/login/google")
async def login_with_google(supabase: Client = Depends(get_supabase)):
try:
response = supabase.auth.sign_in_with_oauth({
"provider": "google",
"options": {
"redirect_to": "https://mhcafqvzbrrwvahpvvzd.supabase.co/auth/v1/callback"
}
})
return {"auth_url": response.url}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@app.post("/auth/logout")
async def logout(user=Depends(get_current_user)):
async def logout(user=Depends(get_current_user), supabase: Client = Depends(get_supabase)):
try:
supabase.auth.sign_out()
return {"message": "Successfully logged out"}
except Exception as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@app.get("/me", response_model=UserOut)
async def get_me(
user = Depends(get_current_user), # Now properly imported
supabase: Client = Depends(get_supabase)
):
try:
# Get user details from public.users table
db_user = supabase.table("users").select("*").eq("id", user.user.id).execute().data[0]
return {
"username": db_user["username"],
"email": user.user.email,
"business_name": db_user["business_name"]
}
except IndexError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found in database"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@app.post("/auth/refresh", response_model=RefreshResponse)
async def refresh_token(refresh_request: RefreshToken):
async def refresh_token(refresh_request: RefreshToken, supabase: Client = Depends(get_supabase)):
"""Refresh the access token using a valid refresh token."""
try:
# Validate the refresh token and get new tokens
@ -165,7 +252,9 @@ async def refresh_token(refresh_request: RefreshToken):
user_data = {
"id": response.user.id,
"email": response.user.email,
"username": response.user.user_metadata.get("username", "Unknown")
"first_name": response.user.user_metadata.get("first_name", "Unknown"),
"last_name": response.user.user_metadata.get("last_name", "Unknown"),
"business_name": response.user.user_metadata.get("business_name", "Unknown")
}
# Return the new tokens and user data

View file

@ -6,5 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi[standard]>=0.115.11",
"pydantic-settings>=2.8.1",
"python-jose>=3.4.0",
"supabase>=2.13.0",
]

View file

@ -26,6 +26,22 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/96/91e93ae5fd04d428c101cdbabce6c820d284d61d2614d00518f4fa52ea24/aiohttp-3.11.14.tar.gz", hash = "sha256:d6edc538c7480fa0a3b2bdd705f8010062d74700198da55d16498e1b49549b9c", size = 7676994 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/ca/e4acb3b41f9e176f50960f7162d656e79bed151b1f911173b2c4a6c0a9d2/aiohttp-3.11.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:70ab0f61c1a73d3e0342cedd9a7321425c27a7067bebeeacd509f96695b875fc", size = 705489 },
{ url = "https://files.pythonhosted.org/packages/84/d5/dcf870e0b11f0c1e3065b7f17673485afa1ddb3d630ccd8f328bccfb459f/aiohttp-3.11.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:602d4db80daf4497de93cb1ce00b8fc79969c0a7cf5b67bec96fa939268d806a", size = 464807 },
{ url = "https://files.pythonhosted.org/packages/7c/f0/dc417d819ae26be6abcd72c28af99d285887fddbf76d4bbe46346f201870/aiohttp-3.11.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a8a0d127c10b8d89e69bbd3430da0f73946d839e65fec00ae48ca7916a31948", size = 456819 },
{ url = "https://files.pythonhosted.org/packages/28/db/f7deb0862ebb821aa3829db20081a122ba67ffd149303f2d5202e30f20cd/aiohttp-3.11.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9f835cdfedcb3f5947304e85b8ca3ace31eef6346d8027a97f4de5fb687534", size = 1683536 },
{ url = "https://files.pythonhosted.org/packages/5e/0d/8bf0619e21c6714902c44ab53e275deb543d4d2e68ab2b7b8fe5ba267506/aiohttp-3.11.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8aa5c68e1e68fff7cd3142288101deb4316b51f03d50c92de6ea5ce646e6c71f", size = 1738111 },
{ url = "https://files.pythonhosted.org/packages/f5/10/204b3700bb57b30b9e759d453fcfb3ad79a3eb18ece4e298aaf7917757dd/aiohttp-3.11.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b512f1de1c688f88dbe1b8bb1283f7fbeb7a2b2b26e743bb2193cbadfa6f307", size = 1794508 },
{ url = "https://files.pythonhosted.org/packages/cc/39/3f65072614c62a315a951fda737e4d9e6e2703f1da0cd2f2d8f629e6092e/aiohttp-3.11.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc9253069158d57e27d47a8453d8a2c5a370dc461374111b5184cf2f147a3cc3", size = 1692006 },
{ url = "https://files.pythonhosted.org/packages/73/77/cc06ecea173f9bee2f20c8e32e2cf4c8e03909a707183cdf95434db4993e/aiohttp-3.11.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b2501f1b981e70932b4a552fc9b3c942991c7ae429ea117e8fba57718cdeed0", size = 1620369 },
{ url = "https://files.pythonhosted.org/packages/87/75/5bd424bcd90c7eb2f50fd752d013db4cefb447deeecfc5bc4e8e0b1c74dd/aiohttp-3.11.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:28a3d083819741592685762d51d789e6155411277050d08066537c5edc4066e6", size = 1642508 },
{ url = "https://files.pythonhosted.org/packages/81/f0/ce936ec575e0569f91e5c8374086a6f7760926f16c3b95428fb55d6bfe91/aiohttp-3.11.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0df3788187559c262922846087e36228b75987f3ae31dd0a1e5ee1034090d42f", size = 1685771 },
{ url = "https://files.pythonhosted.org/packages/68/b7/5216590b99b5b1f18989221c25ac9d9a14a7b0c3c4ae1ff728e906c36430/aiohttp-3.11.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e73fa341d8b308bb799cf0ab6f55fc0461d27a9fa3e4582755a3d81a6af8c09", size = 1648318 },
{ url = "https://files.pythonhosted.org/packages/a5/c2/c27061c4ab93fa25f925c7ebddc10c20d992dbbc329e89d493811299dc93/aiohttp-3.11.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:51ba80d473eb780a329d73ac8afa44aa71dfb521693ccea1dea8b9b5c4df45ce", size = 1704545 },
{ url = "https://files.pythonhosted.org/packages/09/f5/11b2da82f2c52365a5b760a4e944ae50a89cf5fb207024b7853615254584/aiohttp-3.11.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8d1dd75aa4d855c7debaf1ef830ff2dfcc33f893c7db0af2423ee761ebffd22b", size = 1737839 },
{ url = "https://files.pythonhosted.org/packages/03/7f/145e23fe0a4c45b256f14c3268ada5497d487786334721ae8a0c818ee516/aiohttp-3.11.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41cf0cefd9e7b5c646c2ef529c8335e7eafd326f444cc1cdb0c47b6bc836f9be", size = 1695833 },
{ url = "https://files.pythonhosted.org/packages/1c/78/627dba6ee9fb9439e2e29b521adb1135877a9c7b54811fec5c46e59f2fc8/aiohttp-3.11.14-cp312-cp312-win32.whl", hash = "sha256:948abc8952aff63de7b2c83bfe3f211c727da3a33c3a5866a0e2cf1ee1aa950f", size = 412185 },
{ url = "https://files.pythonhosted.org/packages/3f/5f/1737cf6fcf0524693a4aeff8746530b65422236761e7bfdd79c6d2ce2e1c/aiohttp-3.11.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b420d076a46f41ea48e5fcccb996f517af0d406267e31e6716f480a3d50d65c", size = 438526 },
{ url = "https://files.pythonhosted.org/packages/c5/8e/d7f353c5aaf9f868ab382c3d3320dc6efaa639b6b30d5a686bed83196115/aiohttp-3.11.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d14e274828561db91e4178f0057a915f3af1757b94c2ca283cb34cbb6e00b50", size = 698774 },
{ url = "https://files.pythonhosted.org/packages/d5/52/097b98d50f8550883f7d360c6cd4e77668c7442038671bb4b349ced95066/aiohttp-3.11.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f30fc72daf85486cdcdfc3f5e0aea9255493ef499e31582b34abadbfaafb0965", size = 461443 },
{ url = "https://files.pythonhosted.org/packages/2b/5c/19c84bb5796be6ca4fd1432012cfd5f88ec02c8b9e0357cdecc48ff2c4fd/aiohttp-3.11.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4edcbe34e6dba0136e4cabf7568f5a434d89cc9de5d5155371acda275353d228", size = 453717 },
@ -72,6 +88,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
wheels = [
@ -93,12 +110,16 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "fastapi", extra = ["standard"] },
{ name = "pydantic-settings" },
{ name = "python-jose" },
{ name = "supabase" },
]
[package.metadata]
requires-dist = [
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.11" },
{ name = "pydantic-settings", specifier = ">=2.8.1" },
{ name = "python-jose", specifier = ">=3.4.0" },
{ name = "supabase", specifier = ">=2.13.0" },
]
@ -153,6 +174,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 },
]
[[package]]
name = "ecdsa"
version = "0.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607 },
]
[[package]]
name = "email-validator"
version = "2.2.0"
@ -215,6 +248,21 @@ version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 },
{ url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 },
{ url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 },
{ url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 },
{ url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 },
{ url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 },
{ url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 },
{ url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 },
{ url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 },
{ url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 },
{ url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 },
{ url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 },
{ url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 },
{ url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 },
{ url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 },
{ url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 },
{ url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 },
{ url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 },
@ -296,6 +344,13 @@ version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 },
{ url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 },
{ url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 },
{ url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 },
{ url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 },
{ url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 },
{ url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 },
{ url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 },
{ url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 },
{ url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 },
@ -373,6 +428,16 @@ version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
@ -410,6 +475,21 @@ version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/4a/7874ca44a1c9b23796c767dd94159f6c17e31c0e7d090552a1c623247d82/multidict-6.2.0.tar.gz", hash = "sha256:0085b0afb2446e57050140240a8595846ed64d1cbd26cef936bfab3192c673b8", size = 71066 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/e2/0153a8db878aef9b2397be81e62cbc3b32ca9b94e0f700b103027db9d506/multidict-6.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:437c33561edb6eb504b5a30203daf81d4a9b727e167e78b0854d9a4e18e8950b", size = 49204 },
{ url = "https://files.pythonhosted.org/packages/bb/9d/5ccb3224a976d1286f360bb4e89e67b7cdfb87336257fc99be3c17f565d7/multidict-6.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9f49585f4abadd2283034fc605961f40c638635bc60f5162276fec075f2e37a4", size = 29807 },
{ url = "https://files.pythonhosted.org/packages/62/32/ef20037f51b84b074a89bab5af46d4565381c3f825fc7cbfc19c1ee156be/multidict-6.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5dd7106d064d05896ce28c97da3f46caa442fe5a43bc26dfb258e90853b39b44", size = 30000 },
{ url = "https://files.pythonhosted.org/packages/97/81/b0a7560bfc3ec72606232cd7e60159e09b9cf29e66014d770c1315868fa2/multidict-6.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25b11a0417475f093d0f0809a149aff3943c2c56da50fdf2c3c88d57fe3dfbd", size = 131820 },
{ url = "https://files.pythonhosted.org/packages/49/3b/768bfc0e41179fbccd3a22925329a11755b7fdd53bec66dbf6b8772f0bce/multidict-6.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac380cacdd3b183338ba63a144a34e9044520a6fb30c58aa14077157a033c13e", size = 136272 },
{ url = "https://files.pythonhosted.org/packages/71/ac/fd2be3fe98ff54e7739448f771ba730d42036de0870737db9ae34bb8efe9/multidict-6.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61d5541f27533f803a941d3a3f8a3d10ed48c12cf918f557efcbf3cd04ef265c", size = 135233 },
{ url = "https://files.pythonhosted.org/packages/93/76/1657047da771315911a927b364a32dafce4135b79b64208ce4ac69525c56/multidict-6.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:facaf11f21f3a4c51b62931feb13310e6fe3475f85e20d9c9fdce0d2ea561b87", size = 132861 },
{ url = "https://files.pythonhosted.org/packages/19/a5/9f07ffb9bf68b8aaa406c2abee27ad87e8b62a60551587b8e59ee91aea84/multidict-6.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:095a2eabe8c43041d3e6c2cb8287a257b5f1801c2d6ebd1dd877424f1e89cf29", size = 122166 },
{ url = "https://files.pythonhosted.org/packages/95/23/b5ce3318d9d6c8f105c3679510f9d7202980545aad8eb4426313bd8da3ee/multidict-6.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0cc398350ef31167e03f3ca7c19313d4e40a662adcb98a88755e4e861170bdd", size = 136052 },
{ url = "https://files.pythonhosted.org/packages/ce/5c/02cffec58ffe120873dce520af593415b91cc324be0345f534ad3637da4e/multidict-6.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7c611345bbe7cb44aabb877cb94b63e86f2d0db03e382667dbd037866d44b4f8", size = 130094 },
{ url = "https://files.pythonhosted.org/packages/49/f3/3b19a83f4ebf53a3a2a0435f3e447aa227b242ba3fd96a92404b31fb3543/multidict-6.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cd1a0644ccaf27e9d2f6d9c9474faabee21f0578fe85225cc5af9a61e1653df", size = 140962 },
{ url = "https://files.pythonhosted.org/packages/cc/1a/c916b54fb53168c24cb6a3a0795fd99d0a59a0ea93fa9f6edeff5565cb20/multidict-6.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:89b3857652183b8206a891168af47bac10b970d275bba1f6ee46565a758c078d", size = 138082 },
{ url = "https://files.pythonhosted.org/packages/ef/1a/dcb7fb18f64b3727c61f432c1e1a0d52b3924016124e4bbc8a7d2e4fa57b/multidict-6.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:125dd82b40f8c06d08d87b3510beaccb88afac94e9ed4a6f6c71362dc7dbb04b", size = 136019 },
{ url = "https://files.pythonhosted.org/packages/fb/02/7695485375106f5c542574f70e1968c391f86fa3efc9f1fd76aac0af7237/multidict-6.2.0-cp312-cp312-win32.whl", hash = "sha256:76b34c12b013d813e6cb325e6bd4f9c984db27758b16085926bbe7ceeaace626", size = 26676 },
{ url = "https://files.pythonhosted.org/packages/3c/f5/f147000fe1f4078160157b15b0790fff0513646b0f9b7404bf34007a9b44/multidict-6.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:0b183a959fb88ad1be201de2c4bdf52fa8e46e6c185d76201286a97b6f5ee65c", size = 28899 },
{ url = "https://files.pythonhosted.org/packages/a4/6c/5df5590b1f9a821154589df62ceae247537b01ab26b0aa85997c35ca3d9e/multidict-6.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c5e7d2e300d5cb3b2693b6d60d3e8c8e7dd4ebe27cd17c9cb57020cac0acb80", size = 49151 },
{ url = "https://files.pythonhosted.org/packages/d5/ca/c917fbf1be989cd7ea9caa6f87e9c33844ba8d5fbb29cd515d4d2833b84c/multidict-6.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:256d431fe4583c5f1e0f2e9c4d9c22f3a04ae96009b8cfa096da3a8723db0a16", size = 29803 },
{ url = "https://files.pythonhosted.org/packages/22/19/d97086fc96f73acf36d4dbe65c2c4175911969df49c4e94ef082be59d94e/multidict-6.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a3c0ff89fe40a152e77b191b83282c9664357dce3004032d42e68c514ceff27e", size = 29947 },
@ -472,6 +552,22 @@ version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/76/f941e63d55c0293ff7829dd21e7cf1147e90a526756869a9070f287a68c9/propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5", size = 42722 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/2c/921f15dc365796ec23975b322b0078eae72995c7b4d49eba554c6a308d70/propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e", size = 79867 },
{ url = "https://files.pythonhosted.org/packages/11/a5/4a6cc1a559d1f2fb57ea22edc4245158cdffae92f7f92afcee2913f84417/propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af", size = 46109 },
{ url = "https://files.pythonhosted.org/packages/e1/6d/28bfd3af3a567ad7d667348e7f46a520bda958229c4d545ba138a044232f/propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5", size = 45635 },
{ url = "https://files.pythonhosted.org/packages/73/20/d75b42eaffe5075eac2f4e168f6393d21c664c91225288811d85451b2578/propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b", size = 242159 },
{ url = "https://files.pythonhosted.org/packages/a5/fb/4b537dd92f9fd4be68042ec51c9d23885ca5fafe51ec24c58d9401034e5f/propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667", size = 248163 },
{ url = "https://files.pythonhosted.org/packages/e7/af/8a9db04ac596d531ca0ef7dde518feaadfcdabef7b17d6a5ec59ee3effc2/propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7", size = 248794 },
{ url = "https://files.pythonhosted.org/packages/9d/c4/ecfc988879c0fd9db03228725b662d76cf484b6b46f7e92fee94e4b52490/propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7", size = 243912 },
{ url = "https://files.pythonhosted.org/packages/04/a2/298dd27184faa8b7d91cc43488b578db218b3cc85b54d912ed27b8c5597a/propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf", size = 229402 },
{ url = "https://files.pythonhosted.org/packages/be/0d/efe7fec316ca92dbf4bc4a9ba49ca889c43ca6d48ab1d6fa99fc94e5bb98/propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138", size = 226896 },
{ url = "https://files.pythonhosted.org/packages/60/63/72404380ae1d9c96d96e165aa02c66c2aae6072d067fc4713da5cde96762/propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86", size = 221447 },
{ url = "https://files.pythonhosted.org/packages/9d/18/b8392cab6e0964b67a30a8f4dadeaff64dc7022b5a34bb1d004ea99646f4/propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d", size = 222440 },
{ url = "https://files.pythonhosted.org/packages/6f/be/105d9ceda0f97eff8c06bac1673448b2db2a497444de3646464d3f5dc881/propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e", size = 234104 },
{ url = "https://files.pythonhosted.org/packages/cb/c9/f09a4ec394cfcce4053d8b2a04d622b5f22d21ba9bb70edd0cad061fa77b/propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64", size = 239086 },
{ url = "https://files.pythonhosted.org/packages/ea/aa/96f7f9ed6def82db67c972bdb7bd9f28b95d7d98f7e2abaf144c284bf609/propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c", size = 230991 },
{ url = "https://files.pythonhosted.org/packages/5a/11/bee5439de1307d06fad176f7143fec906e499c33d7aff863ea8428b8e98b/propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d", size = 40337 },
{ url = "https://files.pythonhosted.org/packages/e4/17/e5789a54a0455a61cb9efc4ca6071829d992220c2998a27c59aeba749f6f/propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57", size = 44404 },
{ url = "https://files.pythonhosted.org/packages/3a/0f/a79dd23a0efd6ee01ab0dc9750d8479b343bfd0c73560d59d271eb6a99d4/propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568", size = 77287 },
{ url = "https://files.pythonhosted.org/packages/b8/51/76675703c90de38ac75adb8deceb3f3ad99b67ff02a0fa5d067757971ab8/propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9", size = 44923 },
{ url = "https://files.pythonhosted.org/packages/01/9b/fd5ddbee66cf7686e73c516227c2fd9bf471dbfed0f48329d095ea1228d3/propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767", size = 44325 },
@ -507,6 +603,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101 },
]
[[package]]
name = "pyasn1"
version = "0.4.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a4/db/fffec68299e6d7bad3d504147f9094830b704527a7fc098b721d38cc7fa7/pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", size = 146820 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/1e/a94a8d635fa3ce4cfc7f506003548d0a2447ae76fd5ca53932970fe3053f/pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", size = 77145 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
@ -530,6 +635,20 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
@ -546,6 +665,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
]
[[package]]
name = "pydantic-settings"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
]
[[package]]
name = "pygments"
version = "2.19.1"
@ -576,6 +708,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "python-jose"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ecdsa" },
{ name = "pyasn1" },
{ name = "rsa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/a0/c49687cf40cb6128ea4e0559855aff92cd5ebd1a60a31c08526818c0e51e/python-jose-3.4.0.tar.gz", hash = "sha256:9a9a40f418ced8ecaf7e3b28d69887ceaa76adad3bcaa6dae0d9e596fec1d680", size = 92145 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/b0/2586ea6b6fd57a994ece0b56418cbe93fff0efb85e2c9eb6b0caf24a4e37/python_jose-3.4.0-py2.py3-none-any.whl", hash = "sha256:9c9f616819652d109bd889ecd1e15e9a162b9b94d682534c9c2146092945b78f", size = 34616 },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
@ -591,6 +737,15 @@ version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
@ -644,6 +799,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/1b/1c2f43af46456050b27810a7a013af8a7e12bc545a0cdc00eb0df55eb769/rich_toolkit-0.13.2-py3-none-any.whl", hash = "sha256:f3f6c583e5283298a2f7dbd3c65aca18b7f818ad96174113ab5bec0b0e35ed61", size = 13566 },
]
[[package]]
name = "rsa"
version = "4.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 },
]
[[package]]
name = "shellingham"
version = "1.5.4"
@ -789,6 +956,12 @@ version = "0.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 },
{ url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 },
{ url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 },
{ url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 },
{ url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 },
{ url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 },
{ url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 },
{ url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 },
{ url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 },
@ -806,6 +979,19 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 },
{ url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 },
{ url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 },
{ url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 },
{ url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 },
{ url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 },
{ url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 },
{ url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 },
{ url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 },
{ url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 },
{ url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 },
{ url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 },
{ url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 },
{ url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 },
{ url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 },
{ url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 },
@ -826,6 +1012,17 @@ version = "14.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/81/04f7a397653dc8bec94ddc071f34833e8b99b13ef1a3804c149d59f92c18/websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c", size = 163096 },
{ url = "https://files.pythonhosted.org/packages/ec/c5/de30e88557e4d70988ed4d2eabd73fd3e1e52456b9f3a4e9564d86353b6d/websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967", size = 160758 },
{ url = "https://files.pythonhosted.org/packages/e5/8c/d130d668781f2c77d106c007b6c6c1d9db68239107c41ba109f09e6c218a/websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990", size = 160995 },
{ url = "https://files.pythonhosted.org/packages/a6/bc/f6678a0ff17246df4f06765e22fc9d98d1b11a258cc50c5968b33d6742a1/websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda", size = 170815 },
{ url = "https://files.pythonhosted.org/packages/d8/b2/8070cb970c2e4122a6ef38bc5b203415fd46460e025652e1ee3f2f43a9a3/websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95", size = 169759 },
{ url = "https://files.pythonhosted.org/packages/81/da/72f7caabd94652e6eb7e92ed2d3da818626e70b4f2b15a854ef60bf501ec/websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3", size = 170178 },
{ url = "https://files.pythonhosted.org/packages/31/e0/812725b6deca8afd3a08a2e81b3c4c120c17f68c9b84522a520b816cda58/websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9", size = 170453 },
{ url = "https://files.pythonhosted.org/packages/66/d3/8275dbc231e5ba9bb0c4f93144394b4194402a7a0c8ffaca5307a58ab5e3/websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267", size = 169830 },
{ url = "https://files.pythonhosted.org/packages/a3/ae/e7d1a56755ae15ad5a94e80dd490ad09e345365199600b2629b18ee37bc7/websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe", size = 169824 },
{ url = "https://files.pythonhosted.org/packages/b6/32/88ccdd63cb261e77b882e706108d072e4f1c839ed723bf91a3e1f216bf60/websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205", size = 163981 },
{ url = "https://files.pythonhosted.org/packages/b3/7d/32cdb77990b3bdc34a306e0a0f73a1275221e9a66d869f6ff833c95b56ef/websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce", size = 164421 },
{ url = "https://files.pythonhosted.org/packages/82/94/4f9b55099a4603ac53c2912e1f043d6c49d23e94dd82a9ce1eb554a90215/websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e", size = 163102 },
{ url = "https://files.pythonhosted.org/packages/8e/b7/7484905215627909d9a79ae07070057afe477433fdacb59bf608ce86365a/websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad", size = 160766 },
{ url = "https://files.pythonhosted.org/packages/a3/a4/edb62efc84adb61883c7d2c6ad65181cb087c64252138e12d655989eec05/websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03", size = 160998 },
@ -851,6 +1048,22 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 },
{ url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 },
{ url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 },
{ url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 },
{ url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 },
{ url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 },
{ url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 },
{ url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 },
{ url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 },
{ url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 },
{ url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 },
{ url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 },
{ url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 },
{ url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 },
{ url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 },
{ url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 },
{ url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 },
{ url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 },
{ url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 },

View file

@ -49,6 +49,7 @@
"@types/react-router-dom": "^5.3.3",
"axios": "^1.8.4",
"react-router-dom": "^7.3.0",
"react-stately": "^3.36.1"
"react-stately": "^3.36.1",
"ts-pattern": "^5.6.2"
}
}

View file

@ -29,6 +29,9 @@ importers:
react-stately:
specifier: ^3.36.1
version: 3.36.1(react@19.0.0)
ts-pattern:
specifier: ^5.6.2
version: 5.6.2
devDependencies:
'@eslint/js':
specifier: ^9.22.0
@ -2607,6 +2610,9 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
ts-pattern@5.6.2:
resolution: {integrity: sha512-d4IxJUXROL5NCa3amvMg6VQW2HVtZYmUTPfvVtO7zJWGYLJ+mry9v2OmYm+z67aniQoQ8/yFNadiEwtNS9qQiw==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@ -5883,6 +5889,8 @@ snapshots:
dependencies:
typescript: 5.7.3
ts-pattern@5.6.2: {}
tslib@2.8.1: {}
turbo-stream@2.4.0: {}

View file

@ -1,11 +1,10 @@
import { Button } from "./ui-library/button";
import { Header } from "./ui-library/header";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { LoginPage } from "./pages/login";
import { SignUpPage } from "./pages/signup";
import logo from "./assets/icon.jpg";
import { ThemeProvider } from "./contexts/ThemeContext";
import { twMerge } from "tailwind-merge";
import { ResetPasswordPage } from "./pages/reset-password";
import { LandingPage } from "./pages/landing";
export const App = () => {
return (
@ -18,650 +17,10 @@ export const App = () => {
)}
>
<Routes>
<Route
path="/"
element={
<>
<Header />
<div className="container mx-auto px-4">
{/* Hero Section */}
<section className="py-20 text-center relative">
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="w-[500px] h-[500px] bg-emerald-500/10 rounded-full blur-3xl" />
</div>
<h1 className="text-6xl font-bold text-slate-900 dark:text-white mb-6 relative">
Maîtrisez vos dépenses de chantier
</h1>
<p className="text-xl text-slate-700 dark:text-white mb-12 max-w-2xl mx-auto relative">
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.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16 relative">
<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",
"text-lg"
)}
>
Démarrer gratuitement
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg>
</Button>
</div>
<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",
"relative"
)}
>
<div className="aspect-video rounded-lg bg-emerald-100 dark:bg-slate-700/50 animate-pulse" />
</div>
</section>
{/* Features Section */}
<section id="features" className="py-20">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-slate-900 dark:text-white mb-4">
Une solution complète pour votre entreprise
</h2>
<p className="text-xl text-slate-700 dark:text-white max-w-2xl mx-auto">
Découvrez comment XTablo peut transformer votre
gestion des dépenses
</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
<div
className={twMerge(
"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"
)}
>
<div className="w-12 h-12 bg-emerald-500/20 rounded-lg flex items-center justify-center mb-4">
<svg
className="w-6 h-6 text-emerald-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Suivi en temps réel
</h3>
<p className="text-slate-700 dark:text-white">
Visualisez vos dépenses en temps réel et prenez des
décisions éclairées
</p>
</div>
<div
className={twMerge(
"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"
)}
>
<div className="w-12 h-12 bg-emerald-500/20 rounded-lg flex items-center justify-center mb-4">
<svg
className="w-6 h-6 text-emerald-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Rapports personnalisés
</h3>
<p className="text-slate-700 dark:text-white">
Générez des rapports détaillés adaptés à vos besoins
</p>
</div>
<div
className={twMerge(
"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"
)}
>
<div className="w-12 h-12 bg-emerald-500/20 rounded-lg flex items-center justify-center mb-4">
<svg
className="w-6 h-6 text-emerald-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Optimisation des coûts
</h3>
<p className="text-slate-700 dark:text-white">
Identifiez les opportunités d&apos;optimisation et
réduisez vos dépenses
</p>
</div>
</div>
</section>
{/* Testimonials Section */}
<section className="py-20">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-slate-900 dark:text-white mb-4">
Fait confiance par les entreprises du BTP
</h2>
<p className="text-xl text-slate-700 dark:text-white max-w-2xl mx-auto">
Découvrez ce que nos clients disent de XTablo
</p>
</div>
<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">
&quot;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.&quot;
</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">
&quot;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&apos;améliorer significativement nos marges.&quot;
</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">
&quot;La gestion des dépenses sur plusieurs
chantiers n&apos;a jamais é aussi simple. XTablo
nous donne une visibilité totale sur nos
coûts.&quot;
</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>
{/* Pricing Section */}
<section id="pricing" className="py-20">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-slate-900 dark:text-white mb-4">
Des tarifs adaptés à vos besoins
</h2>
<p className="text-xl text-slate-700 dark:text-white max-w-2xl mx-auto">
Choisissez le plan qui correspond le mieux à votre
entreprise
</p>
</div>
<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-emerald-500 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&apos;à 5 chantiers
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-emerald-500 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-emerald-500 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-emerald-500 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-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-emerald-500 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-emerald-500 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-emerald-500 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-emerald-500 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-emerald-500 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-xl shadow-emerald-700/20 mt-auto"
)}
>
Commencer
</Button>
</div>
</div>
</section>
{/* CTA Section */}
<section id="contact" className="py-20 text-center">
<div
className={twMerge(
"bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl p-12",
"border border-emerald-200 dark:border-slate-700/50",
"shadow-xl"
)}
>
<h2 className="text-4xl font-bold text-slate-900 dark:text-white mb-6">
Prêt à optimiser vos dépenses de projet ?
</h2>
<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&apos;hui. Aucune carte bancaire requise.
</p>
<div className="flex flex-col sm:flex-row justify-center gap-6">
<Button
className={twMerge(
"px-8 py-4 bg-emerald-700 rounded-full text-white font-semibold text-lg",
"hover:bg-emerald-600 transition shadow-lg hover:shadow-xl shadow-emerald-700/20"
)}
>
Essai gratuit
</Button>
<Button
className={twMerge(
"px-8 py-4 border-2 border-emerald-700/30 rounded-full text-lg",
"text-slate-900 dark:text-white font-semibold hover:border-emerald-700",
"transition"
)}
>
Contacter les ventes
</Button>
</div>
</div>
</section>
</div>
{/* Footer */}
<footer
className={twMerge(
"py-12 border-t border-slate-200 dark:border-slate-800"
)}
>
<div className="container mx-auto px-4">
<div className="grid md:grid-cols-4 gap-8">
<div>
<div className="flex items-center gap-2 mb-4">
<img
src={logo}
alt="Logo XTablo"
className="w-8 h-8"
/>
<h3 className="text-xl font-bold text-slate-900 dark:text-white">
XTablo
</h3>
</div>
<p className="text-slate-600 dark:text-slate-400">
Optimisez vos dépenses de chantier avec XTablo
</p>
</div>
<div>
<h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
Produit
</h4>
<ul className="space-y-2">
<li>
<a
href="#features"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Fonctionnalités
</a>
</li>
<li>
<a
href="#pricing"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Tarifs
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
Entreprise
</h4>
<ul className="space-y-2">
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
À propos
</a>
</li>
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Blog
</a>
</li>
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Carrières
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
Légal
</h4>
<ul className="space-y-2">
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Confidentialité
</a>
</li>
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Conditions
</a>
</li>
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Cookies
</a>
</li>
</ul>
</div>
</div>
<div className="mt-12 pt-8 border-t border-slate-200 dark:border-slate-800 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>
</div>
</footer>
</>
}
/>
<Route path="/" element={<LandingPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignUpPage />} />
<Route path="/reset-password" element={<ResetPasswordPage />} />
</Routes>
<style>
{`

View file

@ -1,8 +1,11 @@
import "./login-with-google.css";
import { useLoginWithGoogle } from "../../hooks/useAuth";
export function LoginWithGoogle() {
const { mutate: loginWithGoogle } = useLoginWithGoogle();
return (
<button className="gsi-material-button">
<button className="gsi-material-button" onClick={() => loginWithGoogle()}>
<div className="gsi-material-button-state"></div>
<div className="gsi-material-button-content-wrapper">
<div className="gsi-material-button-icon">

View file

@ -1,12 +1,16 @@
import { useMutation } from "@tanstack/react-query";
import { api } from "../lib/api";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { match } from "ts-pattern";
import { toast } from "../ui-library/toast/toast-queue";
interface SignUpData {
email: string;
password: string;
confirm_password: string;
first_name: string;
last_name: string;
company: string;
business_name: string;
}
interface LoginData {
@ -14,20 +18,115 @@ interface LoginData {
password: string;
}
type SignUpErrorCodes = "user_already_exists";
type LoginErrorCodes = "invalid_credentials" | "user_not_found";
export function useSignUp() {
return useMutation({
const [errors, setErrors] = useState<Record<string, string>>({});
const { mutate, isPending } = useMutation<
unknown,
{
response: {
data:
| { human_error: string; form_location: string }[]
| { detail: string };
headers: {
"x-error-code": SignUpErrorCodes;
"x-error-message": string;
};
};
},
SignUpData
>({
mutationFn: async (data: SignUpData) => {
const response = await api.post("/auth/register", data);
return response.data;
},
onSuccess: (data) => {
console.log("data", data);
},
onError: (error) => {
console.log("error", error);
const errMap: Record<string, string> = {};
if (Array.isArray(error.response.data)) {
error.response.data.forEach(
({
human_error,
form_location,
}: {
human_error: string;
form_location: string;
}) => {
errMap[form_location] = human_error;
}
);
} else {
match(error.response.headers["x-error-code"])
.with("user_already_exists", () => {
errMap["email"] = error.response.headers["x-error-message"];
})
.otherwise(() => {
toast.add({
title: "Erreur",
description: error.response.headers["x-error-message"],
type: "error",
position: "top-left",
});
});
}
setErrors(errMap);
},
});
return { mutate, isPending, errors };
}
export function useLogin() {
return useMutation({
export function useLoginEmail() {
const navigate = useNavigate();
const [errors, setErrors] = useState<Record<string, string>>({});
const { mutate, isPending } = useMutation<
unknown,
{
response: {
data: { access_token: string };
headers: { "x-error-code": LoginErrorCodes; "x-error-message": string };
};
},
LoginData
>({
mutationFn: async (data: LoginData) => {
const response = await api.post("/auth/login", data);
return response.data;
},
onSuccess: (data) => {
console.log("data", data);
navigate("/");
},
onError: (error) => {
match(error.response.headers["x-error-code"])
.with("invalid_credentials", () => {
setErrors({ email: error.response.headers["x-error-message"] });
})
.otherwise(() => {
toast.add({
title: "Erreur",
description: error.response.headers["x-error-message"],
type: "error",
position: "top-left",
});
});
},
});
return { mutate, isPending, errors };
}
export function useLoginWithGoogle() {
return useMutation({
mutationFn: async () => {
const response = await api.get("/auth/login/google");
const { auth_url } = response.data;
return auth_url;
},
onSuccess: (data) => {
window.location.href = data;
},
});
}

View file

@ -3,9 +3,10 @@ import { createRoot } from "react-dom/client";
import { App } from "./App";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./lib/api";
import { GlobalToastRegion } from "./ui-library/toast/toast-region";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<GlobalToastRegion />
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>

633
ui/src/pages/landing.tsx Normal file
View file

@ -0,0 +1,633 @@
import { Button } from "../ui-library/button";
import { Header } from "../ui-library/header";
import { twMerge } from "tailwind-merge";
import logo from "../assets/icon.jpg";
export const LandingPage = () => {
return (
<>
<Header />
<div className="container mx-auto px-4">
{/* Hero Section */}
<section className="py-20 text-center relative">
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="w-[500px] h-[500px] bg-emerald-500/10 rounded-full blur-3xl" />
</div>
<h1 className="text-6xl font-bold text-slate-900 dark:text-white mb-6 relative">
Maîtrisez vos dépenses de chantier
</h1>
<p className="text-xl text-slate-700 dark:text-white mb-12 max-w-2xl mx-auto relative">
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.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16 relative">
<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",
"text-lg"
)}
>
Démarrer gratuitement
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 7l5 5m0 0l-5 5m5-5H6"
/>
</svg>
</Button>
</div>
<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",
"relative"
)}
>
<div className="aspect-video rounded-lg bg-emerald-100 dark:bg-slate-700/50 animate-pulse" />
</div>
</section>
{/* Features Section */}
<section id="features" className="py-20">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-slate-900 dark:text-white mb-4">
Une solution complète pour votre entreprise
</h2>
<p className="text-xl text-slate-700 dark:text-white max-w-2xl mx-auto">
Découvrez comment XTablo peut transformer votre gestion des
dépenses
</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
<div
className={twMerge(
"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"
)}
>
<div className="w-12 h-12 bg-emerald-500/20 rounded-lg flex items-center justify-center mb-4">
<svg
className="w-6 h-6 text-emerald-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Suivi en temps réel
</h3>
<p className="text-slate-700 dark:text-white">
Visualisez vos dépenses en temps réel et prenez des décisions
éclairées
</p>
</div>
<div
className={twMerge(
"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"
)}
>
<div className="w-12 h-12 bg-emerald-500/20 rounded-lg flex items-center justify-center mb-4">
<svg
className="w-6 h-6 text-emerald-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Rapports personnalisés
</h3>
<p className="text-slate-700 dark:text-white">
Générez des rapports détaillés adaptés à vos besoins
</p>
</div>
<div
className={twMerge(
"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"
)}
>
<div className="w-12 h-12 bg-emerald-500/20 rounded-lg flex items-center justify-center mb-4">
<svg
className="w-6 h-6 text-emerald-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white mb-2">
Optimisation des coûts
</h3>
<p className="text-slate-700 dark:text-white">
Identifiez les opportunités d&apos;optimisation et réduisez vos
dépenses
</p>
</div>
</div>
</section>
{/* Testimonials Section */}
<section className="py-20">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-slate-900 dark:text-white mb-4">
Fait confiance par les entreprises du BTP
</h2>
<p className="text-xl text-slate-700 dark:text-white max-w-2xl mx-auto">
Découvrez ce que nos clients disent de XTablo
</p>
</div>
<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">
&quot;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.&quot;
</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">
&quot;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&apos;améliorer significativement nos
marges.&quot;
</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">
&quot;La gestion des dépenses sur plusieurs chantiers n&apos;a
jamais é aussi simple. XTablo nous donne une visibilité totale
sur nos coûts.&quot;
</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>
{/* Pricing Section */}
<section id="pricing" className="py-20">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-slate-900 dark:text-white mb-4">
Des tarifs adaptés à vos besoins
</h2>
<p className="text-xl text-slate-700 dark:text-white max-w-2xl mx-auto">
Choisissez le plan qui correspond le mieux à votre entreprise
</p>
</div>
<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-emerald-500 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&apos;à 5 chantiers
</li>
<li className="flex items-center text-slate-700 dark:text-white">
<svg
className="w-5 h-5 text-emerald-500 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-emerald-500 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-emerald-500 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-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-emerald-500 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-emerald-500 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-emerald-500 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-emerald-500 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-emerald-500 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-xl shadow-emerald-700/20 mt-auto"
)}
>
Commencer
</Button>
</div>
</div>
</section>
{/* CTA Section */}
<section id="contact" className="py-20 text-center">
<div
className={twMerge(
"bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl p-12",
"border border-emerald-200 dark:border-slate-700/50",
"shadow-xl"
)}
>
<h2 className="text-4xl font-bold text-slate-900 dark:text-white mb-6">
Prêt à optimiser vos dépenses de projet ?
</h2>
<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&apos;hui. Aucune
carte bancaire requise.
</p>
<div className="flex flex-col sm:flex-row justify-center gap-6">
<Button
className={twMerge(
"px-8 py-4 bg-emerald-700 rounded-full text-white font-semibold text-lg",
"hover:bg-emerald-600 transition shadow-lg hover:shadow-xl shadow-emerald-700/20"
)}
>
Essai gratuit
</Button>
<Button
className={twMerge(
"px-8 py-4 border-2 border-emerald-700/30 rounded-full text-lg",
"text-slate-900 dark:text-white font-semibold hover:border-emerald-700",
"transition"
)}
>
Contacter les ventes
</Button>
</div>
</div>
</section>
</div>
{/* Footer */}
<footer
className={twMerge(
"py-12 border-t border-slate-200 dark:border-slate-800"
)}
>
<div className="container mx-auto px-4">
<div className="grid md:grid-cols-4 gap-8">
<div>
<div className="flex items-center gap-2 mb-4">
<img src={logo} alt="Logo XTablo" className="w-8 h-8" />
<h3 className="text-xl font-bold text-slate-900 dark:text-white">
XTablo
</h3>
</div>
<p className="text-slate-600 dark:text-slate-400">
Optimisez vos dépenses de chantier avec XTablo
</p>
</div>
<div>
<h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
Produit
</h4>
<ul className="space-y-2">
<li>
<a
href="#features"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Fonctionnalités
</a>
</li>
<li>
<a
href="#pricing"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Tarifs
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
Entreprise
</h4>
<ul className="space-y-2">
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
À propos
</a>
</li>
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Blog
</a>
</li>
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Carrières
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
Légal
</h4>
<ul className="space-y-2">
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Confidentialité
</a>
</li>
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Conditions
</a>
</li>
<li>
<a
href="#"
className="text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400"
>
Cookies
</a>
</li>
</ul>
</div>
</div>
<div className="mt-12 pt-8 border-t border-slate-200 dark:border-slate-800 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>
</div>
</footer>
</>
);
};

View file

@ -3,30 +3,25 @@ import { twMerge } from "tailwind-merge";
import { useNavigate } from "react-router-dom";
import { LoginWithGoogle } from "../components/BrandButtons/LoginWIthGoogle";
import { useState } from "react";
import { Label, Input } from "../ui-library/field";
import { useLogin } from "../hooks/useAuth";
import { Label, Input, TextField, FieldError } from "../ui-library/field";
import { useLoginEmail } from "../hooks/useAuth";
import { Form } from "../ui-library/form";
export function LoginPage() {
const navigate = useNavigate();
const login = useLogin();
const { mutate: login, isPending, errors } = useLoginEmail();
const [formData, setFormData] = useState({
email: "",
password: "",
remember: false,
});
const handleSubmit = async (e: React.FormEvent) => {
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
try {
await login.mutateAsync({
email: formData.email,
password: formData.password,
});
navigate("/");
} catch (error) {
console.error("Login failed:", error);
}
login({
email: formData.email,
password: formData.password,
});
};
return (
@ -36,7 +31,7 @@ export function LoginPage() {
>
<div
className={twMerge(
"w-full max-w-md p-8 bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl",
"w-full max-w-lg 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"
)}
@ -60,8 +55,8 @@ export function LoginPage() {
"text-sm font-medium",
"rounded-full",
"relative z-10",
"before:absolute before:w-[100px] before:h-[1px] before:bg-slate-300 dark:before:bg-slate-600 before:left-[-110px] before:top-1/2",
"after:absolute after:w-[100px] after:h-[1px] after:bg-slate-300 dark:after:bg-slate-600 after:right-[-110px] after:top-1/2"
"before:absolute before:w-[120px] before:h-[1px] before:bg-slate-300 dark:before:bg-slate-600 before:left-[-110px] before:top-1/2",
"after:absolute after:w-[120px] after:h-[1px] after:bg-slate-300 dark:after:bg-slate-600 after:right-[-110px] after:top-1/2"
)}
>
Ou continuer avec
@ -69,8 +64,12 @@ export function LoginPage() {
</div>
</div>
<form className="space-y-4" onSubmit={handleSubmit}>
<div>
<Form
className="space-y-4 w-95 max-w-md mx-auto"
onSubmit={onSubmit}
validationErrors={errors}
>
<TextField isRequired name="email">
<Label>Email</Label>
<Input
type="email"
@ -80,9 +79,10 @@ export function LoginPage() {
}
required
/>
</div>
<FieldError />
</TextField>
<div>
<TextField isRequired name="password">
<Label>Mot de passe</Label>
<Input
type="password"
@ -92,11 +92,12 @@ export function LoginPage() {
}
required
/>
</div>
<FieldError />
</TextField>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
<TextField className="flex items-center">
<Input
type="checkbox"
id="remember"
checked={formData.remember}
@ -105,15 +106,15 @@ export function LoginPage() {
}
className="h-4 w-4 text-emerald-600 focus:ring-emerald-500 border-slate-300 rounded"
/>
<label
<Label
htmlFor="remember"
className="ml-2 block text-sm text-slate-700 dark:text-slate-300"
className="ml-2 text-sm text-slate-700 dark:text-slate-300"
>
Se souvenir de moi
</label>
</div>
</Label>
</TextField>
<a
href="#"
href="/reset-password"
className="text-sm text-emerald-600 hover:text-emerald-500"
>
Mot de passe oublié ?
@ -126,11 +127,11 @@ export function LoginPage() {
"hover:bg-emerald-600"
)}
type="submit"
isPending={login.isPending}
pendingLabel="Connexion..."
>
Se connecter
{isPending ? "Connexion..." : "Se connecter"}
</Button>
</form>
</Form>
<p className="text-center text-sm text-slate-600 dark:text-slate-400">
Pas encore de compte ?{" "}

View file

@ -0,0 +1,124 @@
import { Button } from "../ui-library/button";
import { twMerge } from "tailwind-merge";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { Label, Input, TextField, FieldError } from "../ui-library/field";
import { Form } from "../ui-library/form";
import { Text } from "../ui-library/text";
export function ResetPasswordPage() {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [isSubmitted, setIsSubmitted] = useState(false);
const [isPending, setIsPending] = useState(false);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsPending(true);
// TODO: Implement password reset logic
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsSubmitted(true);
setIsPending(false);
};
if (isSubmitted) {
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"
onClick={() => navigate("/login")}
>
<div
className={twMerge(
"w-full max-w-lg 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"
)}
onClick={(e) => e.stopPropagation()}
>
<div className="text-center space-y-4">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white">
Email envoyé
</h1>
<Text className="text-slate-600 dark:text-slate-400">
Si un compte existe avec l&apos;adresse {email}, vous recevrez un
email avec les instructions pour réinitialiser votre mot de passe.
</Text>
<Button
className={twMerge(
"mt-4 bg-emerald-700 text-white",
"hover:bg-emerald-600"
)}
onPress={() => navigate("/login")}
>
Retour à la connexion
</Button>
</div>
</div>
</div>
);
}
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"
onClick={() => navigate("/login")}
>
<div
className={twMerge(
"w-full max-w-lg 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"
)}
onClick={(e) => e.stopPropagation()}
>
<div className="space-y-4">
<div className="text-center">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Mot de passe oublié ?
</h1>
<Text className="text-slate-600 dark:text-slate-400">
Entrez votre adresse email et nous vous enverrons un lien pour
réinitialiser votre mot de passe.
</Text>
</div>
<Form className="space-y-4" onSubmit={onSubmit}>
<TextField isRequired name="email">
<Label>Email</Label>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<FieldError />
</TextField>
<Button
className={twMerge(
"w-full bg-emerald-700 text-white",
"hover:bg-emerald-600"
)}
type="submit"
isPending={isPending}
pendingLabel="Envoi en cours..."
>
Réinitialiser le mot de passe
</Button>
</Form>
<p className="text-center text-sm text-slate-600 dark:text-slate-400">
<a
href="/login"
className="text-emerald-600 hover:text-emerald-500 font-medium"
>
Retour à la connexion
</a>
</p>
</div>
</div>
</div>
);
}

View file

@ -5,10 +5,12 @@ import { LoginWithGoogle } from "../components/BrandButtons/LoginWIthGoogle";
import { useState } from "react";
import { Label, Input, TextField, FieldError } from "../ui-library/field";
import { useSignUp } from "../hooks/useAuth";
import { Form } from "../ui-library/form";
import { Text } from "../ui-library/text";
export function SignUpPage() {
const navigate = useNavigate();
const signUp = useSignUp();
const { mutate: signUp, isPending, errors } = useSignUp();
const [formData, setFormData] = useState({
email: "",
@ -17,27 +19,20 @@ export function SignUpPage() {
username: "",
first_name: "",
last_name: "",
company: "",
business_name: "",
});
const handleSubmit = async (e: React.FormEvent) => {
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (formData.password !== formData.confirmPassword) {
return;
}
try {
await signUp.mutateAsync({
email: formData.email,
password: formData.password,
first_name: formData.first_name,
last_name: formData.last_name,
company: formData.company,
});
navigate("/login");
} catch (error) {
console.error("Registration failed:", error);
}
signUp({
email: formData.email,
password: formData.password,
first_name: formData.first_name,
last_name: formData.last_name,
confirm_password: formData.confirmPassword,
business_name: formData.business_name,
});
};
return (
@ -47,7 +42,7 @@ export function SignUpPage() {
>
<div
className={twMerge(
"w-full max-w-md p-8 bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl",
"w-full max-w-xl 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"
)}
@ -80,9 +75,13 @@ export function SignUpPage() {
</div>
</div>
<form className="space-y-4" onSubmit={handleSubmit}>
<Form
className="space-y-4 w-full"
onSubmit={onSubmit}
validationErrors={errors}
>
<div className="grid grid-cols-2 gap-4">
<div>
<TextField isRequired name="first_name">
<Label>Prénom</Label>
<Input
type="text"
@ -92,8 +91,9 @@ export function SignUpPage() {
}
required
/>
</div>
<div>
<FieldError />
</TextField>
<TextField isRequired name="last_name">
<Label>Nom</Label>
<Input
type="text"
@ -103,22 +103,24 @@ export function SignUpPage() {
}
required
/>
</div>
<FieldError />
</TextField>
</div>
<div>
<TextField isRequired name="business_name">
<Label>Nom de l&apos;entreprise</Label>
<Input
type="text"
value={formData.company}
value={formData.business_name}
onChange={(e) =>
setFormData({ ...formData, company: e.target.value })
setFormData({ ...formData, business_name: e.target.value })
}
required
/>
</div>
<FieldError />
</TextField>
<div>
<TextField isRequired name="email">
<Label>Email professionnel</Label>
<Input
type="email"
@ -128,9 +130,10 @@ export function SignUpPage() {
}
required
/>
</div>
<FieldError />
</TextField>
<div>
<TextField isRequired name="password">
<Label>Mot de passe</Label>
<Input
type="password"
@ -140,11 +143,13 @@ export function SignUpPage() {
}
required
/>
</div>
<FieldError />
<Text slot="description">
Le mot de passe doit contenir au moins 8 caractères.
</Text>
</TextField>
<TextField
isInvalid={formData.password !== formData.confirmPassword}
>
<TextField isRequired name="confirm_password">
<Label>Confirmer le mot de passe</Label>
<Input
type="password"
@ -154,19 +159,19 @@ export function SignUpPage() {
}
required
/>
<FieldError>Les mots de passe ne correspondent pas</FieldError>
<FieldError />
</TextField>
<div className="flex items-start">
<input
<TextField 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"
className="mt-1 mr-2 h-4 w-4 text-emerald-600 focus:ring-emerald-500 border-slate-300 rounded"
required
/>
<label
<Label
htmlFor="terms"
className="ml-2 block text-sm text-slate-700 dark:text-slate-300"
className="text-sm text-slate-700 dark:text-slate-300"
>
J&apos;accepte les{" "}
<a href="#" className="text-emerald-600 hover:text-emerald-500">
@ -176,8 +181,8 @@ export function SignUpPage() {
<a href="#" className="text-emerald-600 hover:text-emerald-500">
politique de confidentialité
</a>
</label>
</div>
</Label>
</TextField>
<Button
className={twMerge(
@ -185,11 +190,12 @@ export function SignUpPage() {
"hover:bg-emerald-600"
)}
type="submit"
isPending={signUp.isPending}
isPending={isPending}
pendingLabel="Création du compte..."
>
Créer mon compte
{isPending ? "Création du compte..." : "Créer mon compte"}
</Button>
</form>
</Form>
<p className="text-center text-sm text-slate-600 dark:text-slate-400">
Déjà un compte ?{" "}

View file

@ -77,7 +77,6 @@ function ToastRegion({ state, ...props }: ToastRegionProps) {
export function GlobalToastRegion(props: AriaToastRegionProps) {
const state = useToastQueue<ToastConfig>(toast);
return state.visibleToasts.length > 0
? createPortal(<ToastRegion {...props} state={state} />, document.body)
: null;
@ -127,7 +126,7 @@ function Toast({ state, ...props }: ToastProps) {
}
const type = props.toast.content.type;
console.log(props.toast);
return (
<div
{...toastProps}