Add reset-password and improve login/signup
This commit is contained in:
parent
29e7741ff5
commit
707d9aa5ac
19 changed files with 1383 additions and 783 deletions
BIN
backend/app/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/__pycache__/config.cpython-312.pyc
Normal file
BIN
backend/app/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
46
backend/app/auth.py
Normal file
46
backend/app/auth.py
Normal 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
16
backend/app/config.py
Normal 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
213
backend/uv.lock
213
backend/uv.lock
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
|
|||
649
ui/src/App.tsx
649
ui/src/App.tsx
|
|
@ -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'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">
|
||||
"XTablo a révolutionné notre gestion des
|
||||
dépenses. Nous pouvons maintenant suivre chaque euro
|
||||
en temps réel et prendre de meilleures décisions
|
||||
pour nos projets de construction."
|
||||
</p>
|
||||
<div className="flex items-center mt-auto">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
|
||||
<div className="ml-3">
|
||||
<p className="text-slate-900 dark:text-white font-medium">
|
||||
Michel Dubois
|
||||
</p>
|
||||
<p className="text-slate-600 dark:text-white/80 text-sm">
|
||||
Chef de Projet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
|
||||
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
|
||||
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
|
||||
)}
|
||||
>
|
||||
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
|
||||
"Les fonctionnalités de suivi des dépenses sont
|
||||
exactement ce dont nous avions besoin. Cela nous a
|
||||
permis de réduire nos coûts de 15% et
|
||||
d'améliorer significativement nos marges."
|
||||
</p>
|
||||
<div className="flex items-center mt-auto">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
|
||||
<div className="ml-3">
|
||||
<p className="text-slate-900 dark:text-white font-medium">
|
||||
Sophie Martin
|
||||
</p>
|
||||
<p className="text-slate-600 dark:text-white/80 text-sm">
|
||||
Directrice Financière
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
|
||||
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
|
||||
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
|
||||
)}
|
||||
>
|
||||
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
|
||||
"La gestion des dépenses sur plusieurs
|
||||
chantiers n'a jamais été aussi simple. XTablo
|
||||
nous donne une visibilité totale sur nos
|
||||
coûts."
|
||||
</p>
|
||||
<div className="flex items-center mt-auto">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
|
||||
<div className="ml-3">
|
||||
<p className="text-slate-900 dark:text-white font-medium">
|
||||
David Laurent
|
||||
</p>
|
||||
<p className="text-slate-600 dark:text-white/80 text-sm">
|
||||
Chef de Chantier
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 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'à 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'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>
|
||||
{`
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
633
ui/src/pages/landing.tsx
Normal 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'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">
|
||||
"XTablo a révolutionné notre gestion des dépenses. Nous
|
||||
pouvons maintenant suivre chaque euro en temps réel et prendre
|
||||
de meilleures décisions pour nos projets de construction."
|
||||
</p>
|
||||
<div className="flex items-center mt-auto">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
|
||||
<div className="ml-3">
|
||||
<p className="text-slate-900 dark:text-white font-medium">
|
||||
Michel Dubois
|
||||
</p>
|
||||
<p className="text-slate-600 dark:text-white/80 text-sm">
|
||||
Chef de Projet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
|
||||
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
|
||||
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
|
||||
)}
|
||||
>
|
||||
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
|
||||
"Les fonctionnalités de suivi des dépenses sont exactement
|
||||
ce dont nous avions besoin. Cela nous a permis de réduire nos
|
||||
coûts de 15% et d'améliorer significativement nos
|
||||
marges."
|
||||
</p>
|
||||
<div className="flex items-center mt-auto">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
|
||||
<div className="ml-3">
|
||||
<p className="text-slate-900 dark:text-white font-medium">
|
||||
Sophie Martin
|
||||
</p>
|
||||
<p className="text-slate-600 dark:text-white/80 text-sm">
|
||||
Directrice Financière
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex-none w-[300px] bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-xl p-6",
|
||||
"hover:bg-emerald-100/50 dark:hover:bg-slate-800/70 transition",
|
||||
"border border-emerald-200 dark:border-slate-700/50 flex flex-col animate-slide"
|
||||
)}
|
||||
>
|
||||
<p className="text-slate-700 dark:text-white mb-4 flex-grow">
|
||||
"La gestion des dépenses sur plusieurs chantiers n'a
|
||||
jamais été aussi simple. XTablo nous donne une visibilité totale
|
||||
sur nos coûts."
|
||||
</p>
|
||||
<div className="flex items-center mt-auto">
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/30" />
|
||||
<div className="ml-3">
|
||||
<p className="text-slate-900 dark:text-white font-medium">
|
||||
David Laurent
|
||||
</p>
|
||||
<p className="text-slate-600 dark:text-white/80 text-sm">
|
||||
Chef de Chantier
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 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'à 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'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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 ?{" "}
|
||||
|
|
|
|||
124
ui/src/pages/reset-password.tsx
Normal file
124
ui/src/pages/reset-password.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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'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'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 ?{" "}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue