Spaces:
Sleeping
Sleeping
darved2305 commited on
Commit ·
adb0589
1
Parent(s): c715979
backend setup in fastapi and authentication in neon for postfres
Browse files- backend/.env +3 -0
- backend/.env.example +3 -0
- backend/app/__init__.py +0 -0
- backend/app/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/app/__pycache__/db.cpython-313.pyc +0 -0
- backend/app/__pycache__/main.cpython-313.pyc +0 -0
- backend/app/__pycache__/models.cpython-313.pyc +0 -0
- backend/app/__pycache__/schemas.cpython-313.pyc +0 -0
- backend/app/__pycache__/security.cpython-313.pyc +0 -0
- backend/app/__pycache__/settings.cpython-313.pyc +0 -0
- backend/app/db.py +15 -0
- backend/app/main.py +28 -0
- backend/app/models.py +24 -0
- backend/app/routes/__init__.py +0 -0
- backend/app/routes/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/app/routes/__pycache__/auth.cpython-313.pyc +0 -0
- backend/app/routes/auth.py +94 -0
- backend/app/schemas.py +25 -0
- backend/app/security.py +25 -0
- backend/app/settings.py +13 -0
- backend/requirements.txt +9 -0
- backend/start.bat +29 -0
- frontend/package-lock.json +339 -1
- frontend/package.json +3 -1
- frontend/src/App.tsx +11 -20
- frontend/src/components/AuthShell.css +184 -0
- frontend/src/components/AuthShell.jsx +46 -0
- frontend/src/components/AuthShell.tsx +56 -0
- frontend/src/components/Header.tsx +6 -5
- frontend/src/components/Hero.tsx +20 -19
- frontend/src/pages/HomePage.tsx +27 -0
- frontend/src/pages/Login.tsx +92 -0
- frontend/src/pages/Signup.tsx +111 -0
- frontend/src/services/auth.js +44 -0
- frontend/src/services/auth.ts +68 -0
backend/.env
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DATABASE_URL=postgresql+asyncpg://neondb_owner:npg_hWF9zPurf2OS@ep-twilight-resonance-ah9o88p9-pooler.c-3.us-east-1.aws.neon.tech/neondb?ssl=require
|
| 2 |
+
JWT_SECRET=ed8afd14a1500cd9d2359010fece9e6f
|
| 3 |
+
FRONTEND_ORIGIN=http://localhost:5174
|
backend/.env.example
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DATABASE_URL=postgresql+asyncpg://username:password@your-neon-host.neon.tech/neondb?sslmode=require
|
| 2 |
+
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
| 3 |
+
FRONTEND_ORIGIN=http://localhost:5173
|
backend/app/__init__.py
ADDED
|
File without changes
|
backend/app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (189 Bytes). View file
|
|
|
backend/app/__pycache__/db.cpython-313.pyc
ADDED
|
Binary file (1.52 kB). View file
|
|
|
backend/app/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (1.3 kB). View file
|
|
|
backend/app/__pycache__/models.cpython-313.pyc
ADDED
|
Binary file (1.66 kB). View file
|
|
|
backend/app/__pycache__/schemas.cpython-313.pyc
ADDED
|
Binary file (1.77 kB). View file
|
|
|
backend/app/__pycache__/security.cpython-313.pyc
ADDED
|
Binary file (2.03 kB). View file
|
|
|
backend/app/__pycache__/settings.cpython-313.pyc
ADDED
|
Binary file (993 Bytes). View file
|
|
|
backend/app/db.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
| 2 |
+
from sqlalchemy.orm import declarative_base
|
| 3 |
+
from app.settings import settings
|
| 4 |
+
|
| 5 |
+
engine = create_async_engine(settings.database_url, echo=False)
|
| 6 |
+
async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
| 7 |
+
Base = declarative_base()
|
| 8 |
+
|
| 9 |
+
async def get_db():
|
| 10 |
+
async with async_session_maker() as session:
|
| 11 |
+
yield session
|
| 12 |
+
|
| 13 |
+
async def init_db():
|
| 14 |
+
async with engine.begin() as conn:
|
| 15 |
+
await conn.run_sync(Base.metadata.create_all)
|
backend/app/main.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from contextlib import asynccontextmanager
|
| 4 |
+
from app.settings import settings
|
| 5 |
+
from app.db import init_db
|
| 6 |
+
from app.routes import auth
|
| 7 |
+
|
| 8 |
+
@asynccontextmanager
|
| 9 |
+
async def lifespan(app: FastAPI):
|
| 10 |
+
await init_db()
|
| 11 |
+
yield
|
| 12 |
+
|
| 13 |
+
app = FastAPI(title="Co-Code GGW Auth API", lifespan=lifespan)
|
| 14 |
+
|
| 15 |
+
# CORS configuration to allow frontend requests
|
| 16 |
+
app.add_middleware(
|
| 17 |
+
CORSMiddleware,
|
| 18 |
+
allow_origins=[settings.frontend_origin],
|
| 19 |
+
allow_credentials=True,
|
| 20 |
+
allow_methods=["*"],
|
| 21 |
+
allow_headers=["*"],
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
app.include_router(auth.router)
|
| 25 |
+
|
| 26 |
+
@app.get("/")
|
| 27 |
+
async def root():
|
| 28 |
+
return {"message": "Co-Code GGW Auth API"}
|
backend/app/models.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String, DateTime, ForeignKey
|
| 2 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import uuid
|
| 5 |
+
from app.db import Base
|
| 6 |
+
|
| 7 |
+
class User(Base):
|
| 8 |
+
__tablename__ = "users"
|
| 9 |
+
|
| 10 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 11 |
+
full_name = Column(String, nullable=False)
|
| 12 |
+
email = Column(String, unique=True, nullable=False, index=True)
|
| 13 |
+
password_hash = Column(String, nullable=False)
|
| 14 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 15 |
+
last_login_at = Column(DateTime, nullable=True)
|
| 16 |
+
|
| 17 |
+
class LoginEvent(Base):
|
| 18 |
+
__tablename__ = "login_events"
|
| 19 |
+
|
| 20 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 21 |
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
| 22 |
+
ip = Column(String, nullable=True)
|
| 23 |
+
user_agent = Column(String, nullable=True)
|
| 24 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
backend/app/routes/__init__.py
ADDED
|
File without changes
|
backend/app/routes/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (196 Bytes). View file
|
|
|
backend/app/routes/__pycache__/auth.cpython-313.pyc
ADDED
|
Binary file (5.37 kB). View file
|
|
|
backend/app/routes/auth.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 2 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 3 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 4 |
+
from sqlalchemy import select
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from app.db import get_db
|
| 7 |
+
from app.models import User, LoginEvent
|
| 8 |
+
from app.schemas import UserCreate, UserLogin, TokenResponse, UserResponse
|
| 9 |
+
from app.security import hash_password, verify_password, create_access_token, decode_access_token
|
| 10 |
+
|
| 11 |
+
router = APIRouter(prefix="/auth", tags=["auth"])
|
| 12 |
+
security = HTTPBearer()
|
| 13 |
+
|
| 14 |
+
@router.post("/signup", response_model=TokenResponse)
|
| 15 |
+
async def signup(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
|
| 16 |
+
result = await db.execute(select(User).where(User.email == user_data.email))
|
| 17 |
+
existing_user = result.scalar_one_or_none()
|
| 18 |
+
|
| 19 |
+
if existing_user:
|
| 20 |
+
raise HTTPException(status_code=400, detail="Email already registered")
|
| 21 |
+
|
| 22 |
+
new_user = User(
|
| 23 |
+
full_name=user_data.full_name,
|
| 24 |
+
email=user_data.email,
|
| 25 |
+
password_hash=hash_password(user_data.password)
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
db.add(new_user)
|
| 29 |
+
await db.commit()
|
| 30 |
+
await db.refresh(new_user)
|
| 31 |
+
|
| 32 |
+
access_token = create_access_token({"sub": str(new_user.id)})
|
| 33 |
+
|
| 34 |
+
return TokenResponse(
|
| 35 |
+
access_token=access_token,
|
| 36 |
+
user=UserResponse.model_validate(new_user)
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
@router.post("/login", response_model=TokenResponse)
|
| 40 |
+
async def login(
|
| 41 |
+
login_data: UserLogin,
|
| 42 |
+
request: Request,
|
| 43 |
+
db: AsyncSession = Depends(get_db)
|
| 44 |
+
):
|
| 45 |
+
result = await db.execute(select(User).where(User.email == login_data.email))
|
| 46 |
+
user = result.scalar_one_or_none()
|
| 47 |
+
|
| 48 |
+
if not user or not verify_password(login_data.password, user.password_hash):
|
| 49 |
+
raise HTTPException(status_code=401, detail="Invalid email or password")
|
| 50 |
+
|
| 51 |
+
user.last_login_at = datetime.utcnow()
|
| 52 |
+
|
| 53 |
+
login_event = LoginEvent(
|
| 54 |
+
user_id=user.id,
|
| 55 |
+
ip=request.client.host if request.client else None,
|
| 56 |
+
user_agent=request.headers.get("user-agent")
|
| 57 |
+
)
|
| 58 |
+
db.add(login_event)
|
| 59 |
+
|
| 60 |
+
await db.commit()
|
| 61 |
+
await db.refresh(user)
|
| 62 |
+
|
| 63 |
+
access_token = create_access_token({"sub": str(user.id)})
|
| 64 |
+
|
| 65 |
+
return TokenResponse(
|
| 66 |
+
access_token=access_token,
|
| 67 |
+
user=UserResponse.model_validate(user)
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
async def get_current_user(
|
| 71 |
+
credentials: HTTPAuthorizationCredentials = Depends(security),
|
| 72 |
+
db: AsyncSession = Depends(get_db)
|
| 73 |
+
) -> User:
|
| 74 |
+
token = credentials.credentials
|
| 75 |
+
payload = decode_access_token(token)
|
| 76 |
+
|
| 77 |
+
if not payload:
|
| 78 |
+
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
| 79 |
+
|
| 80 |
+
user_id = payload.get("sub")
|
| 81 |
+
if not user_id:
|
| 82 |
+
raise HTTPException(status_code=401, detail="Invalid token payload")
|
| 83 |
+
|
| 84 |
+
result = await db.execute(select(User).where(User.id == user_id))
|
| 85 |
+
user = result.scalar_one_or_none()
|
| 86 |
+
|
| 87 |
+
if not user:
|
| 88 |
+
raise HTTPException(status_code=401, detail="User not found")
|
| 89 |
+
|
| 90 |
+
return user
|
| 91 |
+
|
| 92 |
+
@router.get("/me", response_model=UserResponse)
|
| 93 |
+
async def get_me(current_user: User = Depends(get_current_user)):
|
| 94 |
+
return UserResponse.model_validate(current_user)
|
backend/app/schemas.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, EmailStr
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from uuid import UUID
|
| 4 |
+
|
| 5 |
+
class UserBase(BaseModel):
|
| 6 |
+
email: EmailStr
|
| 7 |
+
full_name: str
|
| 8 |
+
|
| 9 |
+
class UserCreate(UserBase):
|
| 10 |
+
password: str
|
| 11 |
+
|
| 12 |
+
class UserLogin(BaseModel):
|
| 13 |
+
email: EmailStr
|
| 14 |
+
password: str
|
| 15 |
+
|
| 16 |
+
class UserResponse(UserBase):
|
| 17 |
+
id: UUID
|
| 18 |
+
created_at: datetime
|
| 19 |
+
|
| 20 |
+
class Config:
|
| 21 |
+
from_attributes = True
|
| 22 |
+
|
| 23 |
+
class TokenResponse(BaseModel):
|
| 24 |
+
access_token: str
|
| 25 |
+
user: UserResponse
|
backend/app/security.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from passlib.context import CryptContext
|
| 2 |
+
from jose import jwt, JWTError
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from app.settings import settings
|
| 5 |
+
|
| 6 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 7 |
+
|
| 8 |
+
def hash_password(password: str) -> str:
|
| 9 |
+
return pwd_context.hash(password)
|
| 10 |
+
|
| 11 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 12 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 13 |
+
|
| 14 |
+
def create_access_token(data: dict) -> str:
|
| 15 |
+
to_encode = data.copy()
|
| 16 |
+
expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
|
| 17 |
+
to_encode.update({"exp": expire})
|
| 18 |
+
return jwt.encode(to_encode, settings.jwt_secret, algorithm=settings.jwt_algorithm)
|
| 19 |
+
|
| 20 |
+
def decode_access_token(token: str) -> dict:
|
| 21 |
+
try:
|
| 22 |
+
payload = jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm])
|
| 23 |
+
return payload
|
| 24 |
+
except JWTError:
|
| 25 |
+
return None
|
backend/app/settings.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
|
| 3 |
+
class Settings(BaseSettings):
|
| 4 |
+
database_url: str
|
| 5 |
+
jwt_secret: str
|
| 6 |
+
frontend_origin: str = "http://localhost:5173"
|
| 7 |
+
jwt_algorithm: str = "HS256"
|
| 8 |
+
access_token_expire_minutes: int = 10080
|
| 9 |
+
|
| 10 |
+
class Config:
|
| 11 |
+
env_file = ".env"
|
| 12 |
+
|
| 13 |
+
settings = Settings()
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn[standard]==0.31.0
|
| 3 |
+
sqlalchemy==2.0.35
|
| 4 |
+
asyncpg==0.29.0
|
| 5 |
+
passlib[bcrypt]==1.7.4
|
| 6 |
+
python-jose[cryptography]==3.3.0
|
| 7 |
+
python-multipart==0.0.12
|
| 8 |
+
pydantic-settings==2.6.0
|
| 9 |
+
python-dotenv==1.0.1
|
backend/start.bat
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
echo Starting Co-Code GGW Backend...
|
| 3 |
+
echo.
|
| 4 |
+
|
| 5 |
+
if not exist venv (
|
| 6 |
+
echo Creating virtual environment...
|
| 7 |
+
python -m venv venv
|
| 8 |
+
echo.
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
echo Activating virtual environment...
|
| 12 |
+
call venv\Scripts\activate
|
| 13 |
+
echo.
|
| 14 |
+
|
| 15 |
+
if not exist app\__pycache__ (
|
| 16 |
+
echo Installing dependencies...
|
| 17 |
+
pip install -r requirements.txt
|
| 18 |
+
echo.
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
if not exist .env (
|
| 22 |
+
echo ERROR: .env file not found!
|
| 23 |
+
echo Please copy .env.example to .env and configure your database settings.
|
| 24 |
+
pause
|
| 25 |
+
exit /b 1
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
echo Starting FastAPI server...
|
| 29 |
+
uvicorn app.main:app --reload --port 8000
|
frontend/package-lock.json
CHANGED
|
@@ -8,13 +8,15 @@
|
|
| 8 |
"name": "co-code-ggw",
|
| 9 |
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
|
|
|
| 11 |
"framer-motion": "^12.29.2",
|
| 12 |
"gsap": "^3.14.2",
|
| 13 |
"i18next": "^25.8.0",
|
| 14 |
"i18next-browser-languagedetector": "^8.2.0",
|
| 15 |
"react": "^18.2.0",
|
| 16 |
"react-dom": "^18.2.0",
|
| 17 |
-
"react-i18next": "^16.5.4"
|
|
|
|
| 18 |
},
|
| 19 |
"devDependencies": {
|
| 20 |
"@types/react": "^18.2.0",
|
|
@@ -1214,6 +1216,23 @@
|
|
| 1214 |
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1215 |
}
|
| 1216 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1217 |
"node_modules/baseline-browser-mapping": {
|
| 1218 |
"version": "2.9.18",
|
| 1219 |
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
|
|
@@ -1258,6 +1277,19 @@
|
|
| 1258 |
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1259 |
}
|
| 1260 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1261 |
"node_modules/caniuse-lite": {
|
| 1262 |
"version": "1.0.30001766",
|
| 1263 |
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
|
@@ -1279,6 +1311,18 @@
|
|
| 1279 |
],
|
| 1280 |
"license": "CC-BY-4.0"
|
| 1281 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1282 |
"node_modules/convert-source-map": {
|
| 1283 |
"version": "2.0.0",
|
| 1284 |
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
|
@@ -1286,6 +1330,19 @@
|
|
| 1286 |
"dev": true,
|
| 1287 |
"license": "MIT"
|
| 1288 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1289 |
"node_modules/csstype": {
|
| 1290 |
"version": "3.2.3",
|
| 1291 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
|
@@ -1311,6 +1368,29 @@
|
|
| 1311 |
}
|
| 1312 |
}
|
| 1313 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1314 |
"node_modules/electron-to-chromium": {
|
| 1315 |
"version": "1.5.279",
|
| 1316 |
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
|
|
@@ -1318,6 +1398,51 @@
|
|
| 1318 |
"dev": true,
|
| 1319 |
"license": "ISC"
|
| 1320 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1321 |
"node_modules/esbuild": {
|
| 1322 |
"version": "0.21.5",
|
| 1323 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
|
@@ -1367,6 +1492,42 @@
|
|
| 1367 |
"node": ">=6"
|
| 1368 |
}
|
| 1369 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1370 |
"node_modules/framer-motion": {
|
| 1371 |
"version": "12.29.2",
|
| 1372 |
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.29.2.tgz",
|
|
@@ -1409,6 +1570,15 @@
|
|
| 1409 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1410 |
}
|
| 1411 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1412 |
"node_modules/gensync": {
|
| 1413 |
"version": "1.0.0-beta.2",
|
| 1414 |
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
|
@@ -1419,12 +1589,100 @@
|
|
| 1419 |
"node": ">=6.9.0"
|
| 1420 |
}
|
| 1421 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1422 |
"node_modules/gsap": {
|
| 1423 |
"version": "3.14.2",
|
| 1424 |
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
|
| 1425 |
"integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==",
|
| 1426 |
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
|
| 1427 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1428 |
"node_modules/html-parse-stringify": {
|
| 1429 |
"version": "3.0.1",
|
| 1430 |
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
|
@@ -1528,6 +1786,36 @@
|
|
| 1528 |
"yallist": "^3.0.2"
|
| 1529 |
}
|
| 1530 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1531 |
"node_modules/motion-dom": {
|
| 1532 |
"version": "12.29.2",
|
| 1533 |
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.29.2.tgz",
|
|
@@ -1612,6 +1900,12 @@
|
|
| 1612 |
"node": "^10 || ^12 || >=14"
|
| 1613 |
}
|
| 1614 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1615 |
"node_modules/react": {
|
| 1616 |
"version": "18.3.1",
|
| 1617 |
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
|
@@ -1674,6 +1968,44 @@
|
|
| 1674 |
"node": ">=0.10.0"
|
| 1675 |
}
|
| 1676 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1677 |
"node_modules/rollup": {
|
| 1678 |
"version": "4.57.0",
|
| 1679 |
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz",
|
|
@@ -1738,6 +2070,12 @@
|
|
| 1738 |
"semver": "bin/semver.js"
|
| 1739 |
}
|
| 1740 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1741 |
"node_modules/source-map-js": {
|
| 1742 |
"version": "1.2.1",
|
| 1743 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
|
|
| 8 |
"name": "co-code-ggw",
|
| 9 |
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
+
"axios": "^1.13.4",
|
| 12 |
"framer-motion": "^12.29.2",
|
| 13 |
"gsap": "^3.14.2",
|
| 14 |
"i18next": "^25.8.0",
|
| 15 |
"i18next-browser-languagedetector": "^8.2.0",
|
| 16 |
"react": "^18.2.0",
|
| 17 |
"react-dom": "^18.2.0",
|
| 18 |
+
"react-i18next": "^16.5.4",
|
| 19 |
+
"react-router-dom": "^7.13.0"
|
| 20 |
},
|
| 21 |
"devDependencies": {
|
| 22 |
"@types/react": "^18.2.0",
|
|
|
|
| 1216 |
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1217 |
}
|
| 1218 |
},
|
| 1219 |
+
"node_modules/asynckit": {
|
| 1220 |
+
"version": "0.4.0",
|
| 1221 |
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
| 1222 |
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
| 1223 |
+
"license": "MIT"
|
| 1224 |
+
},
|
| 1225 |
+
"node_modules/axios": {
|
| 1226 |
+
"version": "1.13.4",
|
| 1227 |
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
|
| 1228 |
+
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
|
| 1229 |
+
"license": "MIT",
|
| 1230 |
+
"dependencies": {
|
| 1231 |
+
"follow-redirects": "^1.15.6",
|
| 1232 |
+
"form-data": "^4.0.4",
|
| 1233 |
+
"proxy-from-env": "^1.1.0"
|
| 1234 |
+
}
|
| 1235 |
+
},
|
| 1236 |
"node_modules/baseline-browser-mapping": {
|
| 1237 |
"version": "2.9.18",
|
| 1238 |
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
|
|
|
|
| 1277 |
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1278 |
}
|
| 1279 |
},
|
| 1280 |
+
"node_modules/call-bind-apply-helpers": {
|
| 1281 |
+
"version": "1.0.2",
|
| 1282 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 1283 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 1284 |
+
"license": "MIT",
|
| 1285 |
+
"dependencies": {
|
| 1286 |
+
"es-errors": "^1.3.0",
|
| 1287 |
+
"function-bind": "^1.1.2"
|
| 1288 |
+
},
|
| 1289 |
+
"engines": {
|
| 1290 |
+
"node": ">= 0.4"
|
| 1291 |
+
}
|
| 1292 |
+
},
|
| 1293 |
"node_modules/caniuse-lite": {
|
| 1294 |
"version": "1.0.30001766",
|
| 1295 |
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
|
|
|
|
| 1311 |
],
|
| 1312 |
"license": "CC-BY-4.0"
|
| 1313 |
},
|
| 1314 |
+
"node_modules/combined-stream": {
|
| 1315 |
+
"version": "1.0.8",
|
| 1316 |
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
| 1317 |
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
| 1318 |
+
"license": "MIT",
|
| 1319 |
+
"dependencies": {
|
| 1320 |
+
"delayed-stream": "~1.0.0"
|
| 1321 |
+
},
|
| 1322 |
+
"engines": {
|
| 1323 |
+
"node": ">= 0.8"
|
| 1324 |
+
}
|
| 1325 |
+
},
|
| 1326 |
"node_modules/convert-source-map": {
|
| 1327 |
"version": "2.0.0",
|
| 1328 |
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
|
|
|
| 1330 |
"dev": true,
|
| 1331 |
"license": "MIT"
|
| 1332 |
},
|
| 1333 |
+
"node_modules/cookie": {
|
| 1334 |
+
"version": "1.1.1",
|
| 1335 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
| 1336 |
+
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
| 1337 |
+
"license": "MIT",
|
| 1338 |
+
"engines": {
|
| 1339 |
+
"node": ">=18"
|
| 1340 |
+
},
|
| 1341 |
+
"funding": {
|
| 1342 |
+
"type": "opencollective",
|
| 1343 |
+
"url": "https://opencollective.com/express"
|
| 1344 |
+
}
|
| 1345 |
+
},
|
| 1346 |
"node_modules/csstype": {
|
| 1347 |
"version": "3.2.3",
|
| 1348 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
|
|
|
| 1368 |
}
|
| 1369 |
}
|
| 1370 |
},
|
| 1371 |
+
"node_modules/delayed-stream": {
|
| 1372 |
+
"version": "1.0.0",
|
| 1373 |
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
| 1374 |
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
| 1375 |
+
"license": "MIT",
|
| 1376 |
+
"engines": {
|
| 1377 |
+
"node": ">=0.4.0"
|
| 1378 |
+
}
|
| 1379 |
+
},
|
| 1380 |
+
"node_modules/dunder-proto": {
|
| 1381 |
+
"version": "1.0.1",
|
| 1382 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 1383 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 1384 |
+
"license": "MIT",
|
| 1385 |
+
"dependencies": {
|
| 1386 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 1387 |
+
"es-errors": "^1.3.0",
|
| 1388 |
+
"gopd": "^1.2.0"
|
| 1389 |
+
},
|
| 1390 |
+
"engines": {
|
| 1391 |
+
"node": ">= 0.4"
|
| 1392 |
+
}
|
| 1393 |
+
},
|
| 1394 |
"node_modules/electron-to-chromium": {
|
| 1395 |
"version": "1.5.279",
|
| 1396 |
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
|
|
|
|
| 1398 |
"dev": true,
|
| 1399 |
"license": "ISC"
|
| 1400 |
},
|
| 1401 |
+
"node_modules/es-define-property": {
|
| 1402 |
+
"version": "1.0.1",
|
| 1403 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 1404 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 1405 |
+
"license": "MIT",
|
| 1406 |
+
"engines": {
|
| 1407 |
+
"node": ">= 0.4"
|
| 1408 |
+
}
|
| 1409 |
+
},
|
| 1410 |
+
"node_modules/es-errors": {
|
| 1411 |
+
"version": "1.3.0",
|
| 1412 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 1413 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 1414 |
+
"license": "MIT",
|
| 1415 |
+
"engines": {
|
| 1416 |
+
"node": ">= 0.4"
|
| 1417 |
+
}
|
| 1418 |
+
},
|
| 1419 |
+
"node_modules/es-object-atoms": {
|
| 1420 |
+
"version": "1.1.1",
|
| 1421 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
| 1422 |
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
| 1423 |
+
"license": "MIT",
|
| 1424 |
+
"dependencies": {
|
| 1425 |
+
"es-errors": "^1.3.0"
|
| 1426 |
+
},
|
| 1427 |
+
"engines": {
|
| 1428 |
+
"node": ">= 0.4"
|
| 1429 |
+
}
|
| 1430 |
+
},
|
| 1431 |
+
"node_modules/es-set-tostringtag": {
|
| 1432 |
+
"version": "2.1.0",
|
| 1433 |
+
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
| 1434 |
+
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
| 1435 |
+
"license": "MIT",
|
| 1436 |
+
"dependencies": {
|
| 1437 |
+
"es-errors": "^1.3.0",
|
| 1438 |
+
"get-intrinsic": "^1.2.6",
|
| 1439 |
+
"has-tostringtag": "^1.0.2",
|
| 1440 |
+
"hasown": "^2.0.2"
|
| 1441 |
+
},
|
| 1442 |
+
"engines": {
|
| 1443 |
+
"node": ">= 0.4"
|
| 1444 |
+
}
|
| 1445 |
+
},
|
| 1446 |
"node_modules/esbuild": {
|
| 1447 |
"version": "0.21.5",
|
| 1448 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
|
|
|
| 1492 |
"node": ">=6"
|
| 1493 |
}
|
| 1494 |
},
|
| 1495 |
+
"node_modules/follow-redirects": {
|
| 1496 |
+
"version": "1.15.11",
|
| 1497 |
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
| 1498 |
+
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
| 1499 |
+
"funding": [
|
| 1500 |
+
{
|
| 1501 |
+
"type": "individual",
|
| 1502 |
+
"url": "https://github.com/sponsors/RubenVerborgh"
|
| 1503 |
+
}
|
| 1504 |
+
],
|
| 1505 |
+
"license": "MIT",
|
| 1506 |
+
"engines": {
|
| 1507 |
+
"node": ">=4.0"
|
| 1508 |
+
},
|
| 1509 |
+
"peerDependenciesMeta": {
|
| 1510 |
+
"debug": {
|
| 1511 |
+
"optional": true
|
| 1512 |
+
}
|
| 1513 |
+
}
|
| 1514 |
+
},
|
| 1515 |
+
"node_modules/form-data": {
|
| 1516 |
+
"version": "4.0.5",
|
| 1517 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
| 1518 |
+
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
| 1519 |
+
"license": "MIT",
|
| 1520 |
+
"dependencies": {
|
| 1521 |
+
"asynckit": "^0.4.0",
|
| 1522 |
+
"combined-stream": "^1.0.8",
|
| 1523 |
+
"es-set-tostringtag": "^2.1.0",
|
| 1524 |
+
"hasown": "^2.0.2",
|
| 1525 |
+
"mime-types": "^2.1.12"
|
| 1526 |
+
},
|
| 1527 |
+
"engines": {
|
| 1528 |
+
"node": ">= 6"
|
| 1529 |
+
}
|
| 1530 |
+
},
|
| 1531 |
"node_modules/framer-motion": {
|
| 1532 |
"version": "12.29.2",
|
| 1533 |
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.29.2.tgz",
|
|
|
|
| 1570 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1571 |
}
|
| 1572 |
},
|
| 1573 |
+
"node_modules/function-bind": {
|
| 1574 |
+
"version": "1.1.2",
|
| 1575 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 1576 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 1577 |
+
"license": "MIT",
|
| 1578 |
+
"funding": {
|
| 1579 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1580 |
+
}
|
| 1581 |
+
},
|
| 1582 |
"node_modules/gensync": {
|
| 1583 |
"version": "1.0.0-beta.2",
|
| 1584 |
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
|
|
|
| 1589 |
"node": ">=6.9.0"
|
| 1590 |
}
|
| 1591 |
},
|
| 1592 |
+
"node_modules/get-intrinsic": {
|
| 1593 |
+
"version": "1.3.0",
|
| 1594 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 1595 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 1596 |
+
"license": "MIT",
|
| 1597 |
+
"dependencies": {
|
| 1598 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 1599 |
+
"es-define-property": "^1.0.1",
|
| 1600 |
+
"es-errors": "^1.3.0",
|
| 1601 |
+
"es-object-atoms": "^1.1.1",
|
| 1602 |
+
"function-bind": "^1.1.2",
|
| 1603 |
+
"get-proto": "^1.0.1",
|
| 1604 |
+
"gopd": "^1.2.0",
|
| 1605 |
+
"has-symbols": "^1.1.0",
|
| 1606 |
+
"hasown": "^2.0.2",
|
| 1607 |
+
"math-intrinsics": "^1.1.0"
|
| 1608 |
+
},
|
| 1609 |
+
"engines": {
|
| 1610 |
+
"node": ">= 0.4"
|
| 1611 |
+
},
|
| 1612 |
+
"funding": {
|
| 1613 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1614 |
+
}
|
| 1615 |
+
},
|
| 1616 |
+
"node_modules/get-proto": {
|
| 1617 |
+
"version": "1.0.1",
|
| 1618 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 1619 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 1620 |
+
"license": "MIT",
|
| 1621 |
+
"dependencies": {
|
| 1622 |
+
"dunder-proto": "^1.0.1",
|
| 1623 |
+
"es-object-atoms": "^1.0.0"
|
| 1624 |
+
},
|
| 1625 |
+
"engines": {
|
| 1626 |
+
"node": ">= 0.4"
|
| 1627 |
+
}
|
| 1628 |
+
},
|
| 1629 |
+
"node_modules/gopd": {
|
| 1630 |
+
"version": "1.2.0",
|
| 1631 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 1632 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 1633 |
+
"license": "MIT",
|
| 1634 |
+
"engines": {
|
| 1635 |
+
"node": ">= 0.4"
|
| 1636 |
+
},
|
| 1637 |
+
"funding": {
|
| 1638 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1639 |
+
}
|
| 1640 |
+
},
|
| 1641 |
"node_modules/gsap": {
|
| 1642 |
"version": "3.14.2",
|
| 1643 |
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
|
| 1644 |
"integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==",
|
| 1645 |
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
|
| 1646 |
},
|
| 1647 |
+
"node_modules/has-symbols": {
|
| 1648 |
+
"version": "1.1.0",
|
| 1649 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 1650 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 1651 |
+
"license": "MIT",
|
| 1652 |
+
"engines": {
|
| 1653 |
+
"node": ">= 0.4"
|
| 1654 |
+
},
|
| 1655 |
+
"funding": {
|
| 1656 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1657 |
+
}
|
| 1658 |
+
},
|
| 1659 |
+
"node_modules/has-tostringtag": {
|
| 1660 |
+
"version": "1.0.2",
|
| 1661 |
+
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
| 1662 |
+
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
| 1663 |
+
"license": "MIT",
|
| 1664 |
+
"dependencies": {
|
| 1665 |
+
"has-symbols": "^1.0.3"
|
| 1666 |
+
},
|
| 1667 |
+
"engines": {
|
| 1668 |
+
"node": ">= 0.4"
|
| 1669 |
+
},
|
| 1670 |
+
"funding": {
|
| 1671 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1672 |
+
}
|
| 1673 |
+
},
|
| 1674 |
+
"node_modules/hasown": {
|
| 1675 |
+
"version": "2.0.2",
|
| 1676 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
| 1677 |
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 1678 |
+
"license": "MIT",
|
| 1679 |
+
"dependencies": {
|
| 1680 |
+
"function-bind": "^1.1.2"
|
| 1681 |
+
},
|
| 1682 |
+
"engines": {
|
| 1683 |
+
"node": ">= 0.4"
|
| 1684 |
+
}
|
| 1685 |
+
},
|
| 1686 |
"node_modules/html-parse-stringify": {
|
| 1687 |
"version": "3.0.1",
|
| 1688 |
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
|
|
|
| 1786 |
"yallist": "^3.0.2"
|
| 1787 |
}
|
| 1788 |
},
|
| 1789 |
+
"node_modules/math-intrinsics": {
|
| 1790 |
+
"version": "1.1.0",
|
| 1791 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 1792 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 1793 |
+
"license": "MIT",
|
| 1794 |
+
"engines": {
|
| 1795 |
+
"node": ">= 0.4"
|
| 1796 |
+
}
|
| 1797 |
+
},
|
| 1798 |
+
"node_modules/mime-db": {
|
| 1799 |
+
"version": "1.52.0",
|
| 1800 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
| 1801 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
| 1802 |
+
"license": "MIT",
|
| 1803 |
+
"engines": {
|
| 1804 |
+
"node": ">= 0.6"
|
| 1805 |
+
}
|
| 1806 |
+
},
|
| 1807 |
+
"node_modules/mime-types": {
|
| 1808 |
+
"version": "2.1.35",
|
| 1809 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
| 1810 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
| 1811 |
+
"license": "MIT",
|
| 1812 |
+
"dependencies": {
|
| 1813 |
+
"mime-db": "1.52.0"
|
| 1814 |
+
},
|
| 1815 |
+
"engines": {
|
| 1816 |
+
"node": ">= 0.6"
|
| 1817 |
+
}
|
| 1818 |
+
},
|
| 1819 |
"node_modules/motion-dom": {
|
| 1820 |
"version": "12.29.2",
|
| 1821 |
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.29.2.tgz",
|
|
|
|
| 1900 |
"node": "^10 || ^12 || >=14"
|
| 1901 |
}
|
| 1902 |
},
|
| 1903 |
+
"node_modules/proxy-from-env": {
|
| 1904 |
+
"version": "1.1.0",
|
| 1905 |
+
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
| 1906 |
+
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
| 1907 |
+
"license": "MIT"
|
| 1908 |
+
},
|
| 1909 |
"node_modules/react": {
|
| 1910 |
"version": "18.3.1",
|
| 1911 |
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
|
|
|
| 1968 |
"node": ">=0.10.0"
|
| 1969 |
}
|
| 1970 |
},
|
| 1971 |
+
"node_modules/react-router": {
|
| 1972 |
+
"version": "7.13.0",
|
| 1973 |
+
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
|
| 1974 |
+
"integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
|
| 1975 |
+
"license": "MIT",
|
| 1976 |
+
"dependencies": {
|
| 1977 |
+
"cookie": "^1.0.1",
|
| 1978 |
+
"set-cookie-parser": "^2.6.0"
|
| 1979 |
+
},
|
| 1980 |
+
"engines": {
|
| 1981 |
+
"node": ">=20.0.0"
|
| 1982 |
+
},
|
| 1983 |
+
"peerDependencies": {
|
| 1984 |
+
"react": ">=18",
|
| 1985 |
+
"react-dom": ">=18"
|
| 1986 |
+
},
|
| 1987 |
+
"peerDependenciesMeta": {
|
| 1988 |
+
"react-dom": {
|
| 1989 |
+
"optional": true
|
| 1990 |
+
}
|
| 1991 |
+
}
|
| 1992 |
+
},
|
| 1993 |
+
"node_modules/react-router-dom": {
|
| 1994 |
+
"version": "7.13.0",
|
| 1995 |
+
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
|
| 1996 |
+
"integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
|
| 1997 |
+
"license": "MIT",
|
| 1998 |
+
"dependencies": {
|
| 1999 |
+
"react-router": "7.13.0"
|
| 2000 |
+
},
|
| 2001 |
+
"engines": {
|
| 2002 |
+
"node": ">=20.0.0"
|
| 2003 |
+
},
|
| 2004 |
+
"peerDependencies": {
|
| 2005 |
+
"react": ">=18",
|
| 2006 |
+
"react-dom": ">=18"
|
| 2007 |
+
}
|
| 2008 |
+
},
|
| 2009 |
"node_modules/rollup": {
|
| 2010 |
"version": "4.57.0",
|
| 2011 |
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz",
|
|
|
|
| 2070 |
"semver": "bin/semver.js"
|
| 2071 |
}
|
| 2072 |
},
|
| 2073 |
+
"node_modules/set-cookie-parser": {
|
| 2074 |
+
"version": "2.7.2",
|
| 2075 |
+
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
| 2076 |
+
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
| 2077 |
+
"license": "MIT"
|
| 2078 |
+
},
|
| 2079 |
"node_modules/source-map-js": {
|
| 2080 |
"version": "1.2.1",
|
| 2081 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
frontend/package.json
CHANGED
|
@@ -9,13 +9,15 @@
|
|
| 9 |
"preview": "vite preview"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
|
|
|
| 12 |
"framer-motion": "^12.29.2",
|
| 13 |
"gsap": "^3.14.2",
|
| 14 |
"i18next": "^25.8.0",
|
| 15 |
"i18next-browser-languagedetector": "^8.2.0",
|
| 16 |
"react": "^18.2.0",
|
| 17 |
"react-dom": "^18.2.0",
|
| 18 |
-
"react-i18next": "^16.5.4"
|
|
|
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
| 21 |
"@types/react": "^18.2.0",
|
|
|
|
| 9 |
"preview": "vite preview"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
+
"axios": "^1.13.4",
|
| 13 |
"framer-motion": "^12.29.2",
|
| 14 |
"gsap": "^3.14.2",
|
| 15 |
"i18next": "^25.8.0",
|
| 16 |
"i18next-browser-languagedetector": "^8.2.0",
|
| 17 |
"react": "^18.2.0",
|
| 18 |
"react-dom": "^18.2.0",
|
| 19 |
+
"react-i18next": "^16.5.4",
|
| 20 |
+
"react-router-dom": "^7.13.0"
|
| 21 |
},
|
| 22 |
"devDependencies": {
|
| 23 |
"@types/react": "^18.2.0",
|
frontend/src/App.tsx
CHANGED
|
@@ -1,27 +1,18 @@
|
|
| 1 |
-
import
|
| 2 |
-
import
|
| 3 |
-
import
|
| 4 |
-
import
|
| 5 |
-
import BeliefStatement from './components/BeliefStatement'
|
| 6 |
-
import Testimonials from './components/Testimonials'
|
| 7 |
-
import ArtOfNaturalRemedies from './components/ArtOfNaturalRemedies'
|
| 8 |
-
import Footer from './components/Footer'
|
| 9 |
import './App.css'
|
| 10 |
|
| 11 |
function App() {
|
| 12 |
return (
|
| 13 |
-
<
|
| 14 |
-
<
|
| 15 |
-
|
| 16 |
-
<
|
| 17 |
-
<
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
<Testimonials />
|
| 21 |
-
<ArtOfNaturalRemedies />
|
| 22 |
-
</main>
|
| 23 |
-
<Footer />
|
| 24 |
-
</div>
|
| 25 |
)
|
| 26 |
}
|
| 27 |
|
|
|
|
| 1 |
+
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
| 2 |
+
import HomePage from './pages/HomePage'
|
| 3 |
+
import Login from './pages/Login'
|
| 4 |
+
import Signup from './pages/Signup'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import './App.css'
|
| 6 |
|
| 7 |
function App() {
|
| 8 |
return (
|
| 9 |
+
<Router>
|
| 10 |
+
<Routes>
|
| 11 |
+
<Route path="/" element={<HomePage />} />
|
| 12 |
+
<Route path="/login" element={<Login />} />
|
| 13 |
+
<Route path="/signup" element={<Signup />} />
|
| 14 |
+
</Routes>
|
| 15 |
+
</Router>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
)
|
| 17 |
}
|
| 18 |
|
frontend/src/components/AuthShell.css
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.auth-shell {
|
| 2 |
+
min-height: 100vh;
|
| 3 |
+
background: linear-gradient(135deg, #f5f1e8 0%, #e8e0d5 100%);
|
| 4 |
+
display: flex;
|
| 5 |
+
align-items: center;
|
| 6 |
+
justify-content: center;
|
| 7 |
+
padding: 2rem;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.auth-container {
|
| 11 |
+
width: 100%;
|
| 12 |
+
max-width: 480px;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.auth-header {
|
| 16 |
+
text-align: center;
|
| 17 |
+
margin-bottom: 2rem;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.auth-logo {
|
| 21 |
+
display: inline-flex;
|
| 22 |
+
flex-direction: column;
|
| 23 |
+
text-decoration: none;
|
| 24 |
+
color: #2d3e2f;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.logo-main {
|
| 28 |
+
font-family: 'DM Serif Display', serif;
|
| 29 |
+
font-size: 2rem;
|
| 30 |
+
font-weight: 400;
|
| 31 |
+
letter-spacing: 0.02em;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.logo-sub {
|
| 35 |
+
font-family: 'DM Sans', sans-serif;
|
| 36 |
+
font-size: 0.875rem;
|
| 37 |
+
letter-spacing: 0.15em;
|
| 38 |
+
opacity: 0.7;
|
| 39 |
+
margin-top: -0.25rem;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.auth-card {
|
| 43 |
+
background: rgba(255, 255, 255, 0.95);
|
| 44 |
+
border-radius: 24px;
|
| 45 |
+
padding: 3rem 2.5rem;
|
| 46 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
|
| 47 |
+
backdrop-filter: blur(10px);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.auth-title {
|
| 51 |
+
font-family: 'DM Serif Display', serif;
|
| 52 |
+
font-size: 2rem;
|
| 53 |
+
font-weight: 400;
|
| 54 |
+
color: #2d3e2f;
|
| 55 |
+
margin: 0 0 0.5rem 0;
|
| 56 |
+
text-align: center;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.auth-subtitle {
|
| 60 |
+
font-family: 'DM Sans', sans-serif;
|
| 61 |
+
font-size: 0.9375rem;
|
| 62 |
+
color: #6b7b6d;
|
| 63 |
+
margin: 0 0 2rem 0;
|
| 64 |
+
text-align: center;
|
| 65 |
+
line-height: 1.5;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.auth-form {
|
| 69 |
+
display: flex;
|
| 70 |
+
flex-direction: column;
|
| 71 |
+
gap: 1.25rem;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.form-group {
|
| 75 |
+
display: flex;
|
| 76 |
+
flex-direction: column;
|
| 77 |
+
gap: 0.5rem;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.form-label {
|
| 81 |
+
font-family: 'DM Sans', sans-serif;
|
| 82 |
+
font-size: 0.875rem;
|
| 83 |
+
font-weight: 500;
|
| 84 |
+
color: #2d3e2f;
|
| 85 |
+
letter-spacing: 0.01em;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.form-input {
|
| 89 |
+
font-family: 'DM Sans', sans-serif;
|
| 90 |
+
font-size: 0.9375rem;
|
| 91 |
+
padding: 0.875rem 1rem;
|
| 92 |
+
border: 1.5px solid #d8d0c4;
|
| 93 |
+
border-radius: 12px;
|
| 94 |
+
background: #ffffff;
|
| 95 |
+
color: #2d3e2f;
|
| 96 |
+
transition: all 0.3s ease;
|
| 97 |
+
outline: none;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.form-input:focus {
|
| 101 |
+
border-color: #7ba88a;
|
| 102 |
+
box-shadow: 0 0 0 3px rgba(123, 168, 138, 0.1);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.form-input::placeholder {
|
| 106 |
+
color: #a8b5aa;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.form-error {
|
| 110 |
+
font-family: 'DM Sans', sans-serif;
|
| 111 |
+
font-size: 0.8125rem;
|
| 112 |
+
color: #d64545;
|
| 113 |
+
margin-top: -0.5rem;
|
| 114 |
+
padding-left: 0.25rem;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.auth-button {
|
| 118 |
+
font-family: 'DM Sans', sans-serif;
|
| 119 |
+
font-size: 0.9375rem;
|
| 120 |
+
font-weight: 500;
|
| 121 |
+
padding: 1rem 2rem;
|
| 122 |
+
background: linear-gradient(135deg, #7ba88a 0%, #5d8a6f 100%);
|
| 123 |
+
color: white;
|
| 124 |
+
border: none;
|
| 125 |
+
border-radius: 12px;
|
| 126 |
+
cursor: pointer;
|
| 127 |
+
transition: all 0.3s ease;
|
| 128 |
+
margin-top: 0.5rem;
|
| 129 |
+
letter-spacing: 0.02em;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.auth-button:hover:not(:disabled) {
|
| 133 |
+
transform: translateY(-2px);
|
| 134 |
+
box-shadow: 0 8px 24px rgba(123, 168, 138, 0.3);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.auth-button:active:not(:disabled) {
|
| 138 |
+
transform: translateY(0);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.auth-button:disabled {
|
| 142 |
+
opacity: 0.6;
|
| 143 |
+
cursor: not-allowed;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.auth-footer {
|
| 147 |
+
margin-top: 2rem;
|
| 148 |
+
padding-top: 2rem;
|
| 149 |
+
border-top: 1px solid #e8e0d5;
|
| 150 |
+
text-align: center;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.auth-footer p {
|
| 154 |
+
font-family: 'DM Sans', sans-serif;
|
| 155 |
+
font-size: 0.875rem;
|
| 156 |
+
color: #6b7b6d;
|
| 157 |
+
margin: 0;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.auth-link {
|
| 161 |
+
color: #7ba88a;
|
| 162 |
+
text-decoration: none;
|
| 163 |
+
font-weight: 500;
|
| 164 |
+
transition: color 0.3s ease;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.auth-link:hover {
|
| 168 |
+
color: #5d8a6f;
|
| 169 |
+
text-decoration: underline;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
@media (max-width: 640px) {
|
| 173 |
+
.auth-shell {
|
| 174 |
+
padding: 1rem;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.auth-card {
|
| 178 |
+
padding: 2rem 1.5rem;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.auth-title {
|
| 182 |
+
font-size: 1.75rem;
|
| 183 |
+
}
|
| 184 |
+
}
|
frontend/src/components/AuthShell.jsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'framer-motion'
|
| 2 |
+
import { Link } from 'react-router-dom'
|
| 3 |
+
import './AuthShell.css'
|
| 4 |
+
|
| 5 |
+
function AuthShell({ children, title, subtitle, footerText, footerLink, footerLinkText }) {
|
| 6 |
+
return (
|
| 7 |
+
<div className="auth-shell">
|
| 8 |
+
<div className="auth-container">
|
| 9 |
+
<motion.div
|
| 10 |
+
className="auth-header"
|
| 11 |
+
initial={{ opacity: 0, y: -20 }}
|
| 12 |
+
animate={{ opacity: 1, y: 0 }}
|
| 13 |
+
transition={{ duration: 0.6 }}
|
| 14 |
+
>
|
| 15 |
+
<Link to="/" className="auth-logo">
|
| 16 |
+
<span className="logo-main">Co-Code</span>
|
| 17 |
+
<span className="logo-sub">GGW</span>
|
| 18 |
+
</Link>
|
| 19 |
+
</motion.div>
|
| 20 |
+
|
| 21 |
+
<motion.div
|
| 22 |
+
className="auth-card"
|
| 23 |
+
initial={{ opacity: 0, y: 40 }}
|
| 24 |
+
animate={{ opacity: 1, y: 0 }}
|
| 25 |
+
transition={{ duration: 0.8, delay: 0.2 }}
|
| 26 |
+
>
|
| 27 |
+
<h1 className="auth-title">{title}</h1>
|
| 28 |
+
<p className="auth-subtitle">{subtitle}</p>
|
| 29 |
+
|
| 30 |
+
{children}
|
| 31 |
+
|
| 32 |
+
<div className="auth-footer">
|
| 33 |
+
<p>
|
| 34 |
+
{footerText}{' '}
|
| 35 |
+
<Link to={footerLink} className="auth-link">
|
| 36 |
+
{footerLinkText}
|
| 37 |
+
</Link>
|
| 38 |
+
</p>
|
| 39 |
+
</div>
|
| 40 |
+
</motion.div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export default AuthShell
|
frontend/src/components/AuthShell.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'framer-motion'
|
| 2 |
+
import { Link } from 'react-router-dom'
|
| 3 |
+
import { ReactNode } from 'react'
|
| 4 |
+
import './AuthShell.css'
|
| 5 |
+
|
| 6 |
+
interface AuthShellProps {
|
| 7 |
+
children: ReactNode
|
| 8 |
+
title: string
|
| 9 |
+
subtitle: string
|
| 10 |
+
footerText: string
|
| 11 |
+
footerLink: string
|
| 12 |
+
footerLinkText: string
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
function AuthShell({ children, title, subtitle, footerText, footerLink, footerLinkText }: AuthShellProps) {
|
| 16 |
+
return (
|
| 17 |
+
<div className="auth-shell">
|
| 18 |
+
<div className="auth-container">
|
| 19 |
+
<motion.div
|
| 20 |
+
className="auth-header"
|
| 21 |
+
initial={{ opacity: 0, y: -20 }}
|
| 22 |
+
animate={{ opacity: 1, y: 0 }}
|
| 23 |
+
transition={{ duration: 0.6 }}
|
| 24 |
+
>
|
| 25 |
+
<Link to="/" className="auth-logo">
|
| 26 |
+
<span className="logo-main">Co-Code</span>
|
| 27 |
+
<span className="logo-sub">GGW</span>
|
| 28 |
+
</Link>
|
| 29 |
+
</motion.div>
|
| 30 |
+
|
| 31 |
+
<motion.div
|
| 32 |
+
className="auth-card"
|
| 33 |
+
initial={{ opacity: 0, y: 40 }}
|
| 34 |
+
animate={{ opacity: 1, y: 0 }}
|
| 35 |
+
transition={{ duration: 0.8, delay: 0.2 }}
|
| 36 |
+
>
|
| 37 |
+
<h1 className="auth-title">{title}</h1>
|
| 38 |
+
<p className="auth-subtitle">{subtitle}</p>
|
| 39 |
+
|
| 40 |
+
{children}
|
| 41 |
+
|
| 42 |
+
<div className="auth-footer">
|
| 43 |
+
<p>
|
| 44 |
+
{footerText}{' '}
|
| 45 |
+
<Link to={footerLink} className="auth-link">
|
| 46 |
+
{footerLinkText}
|
| 47 |
+
</Link>
|
| 48 |
+
</p>
|
| 49 |
+
</div>
|
| 50 |
+
</motion.div>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
)
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export default AuthShell
|
frontend/src/components/Header.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { motion } from 'framer-motion'
|
| 2 |
import { useTranslation } from 'react-i18next'
|
|
|
|
| 3 |
import LanguageSwitcher from './LanguageSwitcher'
|
| 4 |
import './Header.css'
|
| 5 |
|
|
@@ -63,15 +64,15 @@ function Header() {
|
|
| 63 |
transition={{ delay: 0.5, duration: 0.6 }}
|
| 64 |
>
|
| 65 |
<LanguageSwitcher />
|
| 66 |
-
<motion.
|
| 67 |
-
href="#"
|
| 68 |
-
className="book-session-btn"
|
| 69 |
whileHover={{ scale: 1.08, boxShadow: '0 8px 25px rgba(74, 124, 89, 0.3)' }}
|
| 70 |
whileTap={{ scale: 0.95 }}
|
| 71 |
transition={{ type: 'spring', stiffness: 400, damping: 10 }}
|
| 72 |
>
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
| 75 |
</motion.div>
|
| 76 |
</div>
|
| 77 |
</motion.header>
|
|
|
|
| 1 |
import { motion } from 'framer-motion'
|
| 2 |
import { useTranslation } from 'react-i18next'
|
| 3 |
+
import { Link } from 'react-router-dom'
|
| 4 |
import LanguageSwitcher from './LanguageSwitcher'
|
| 5 |
import './Header.css'
|
| 6 |
|
|
|
|
| 64 |
transition={{ delay: 0.5, duration: 0.6 }}
|
| 65 |
>
|
| 66 |
<LanguageSwitcher />
|
| 67 |
+
<motion.div
|
|
|
|
|
|
|
| 68 |
whileHover={{ scale: 1.08, boxShadow: '0 8px 25px rgba(74, 124, 89, 0.3)' }}
|
| 69 |
whileTap={{ scale: 0.95 }}
|
| 70 |
transition={{ type: 'spring', stiffness: 400, damping: 10 }}
|
| 71 |
>
|
| 72 |
+
<Link to="/login" className="book-session-btn">
|
| 73 |
+
{t('header.cta')}
|
| 74 |
+
</Link>
|
| 75 |
+
</motion.div>
|
| 76 |
</motion.div>
|
| 77 |
</div>
|
| 78 |
</motion.header>
|
frontend/src/components/Hero.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { useEffect, useRef } from 'react'
|
| 2 |
import { motion } from 'framer-motion'
|
| 3 |
import { useTranslation } from 'react-i18next'
|
|
|
|
| 4 |
import gsap from 'gsap'
|
| 5 |
import './Hero.css'
|
| 6 |
|
|
@@ -95,32 +96,32 @@ function Hero() {
|
|
| 95 |
animate={{ opacity: 1, y: 0 }}
|
| 96 |
transition={{ delay: 1.3, duration: 0.8 }}
|
| 97 |
>
|
| 98 |
-
<motion.
|
| 99 |
-
href="#"
|
| 100 |
-
className="btn-primary"
|
| 101 |
whileHover={{ scale: 1.1, y: -3, boxShadow: '0 10px 30px rgba(74, 124, 89, 0.4)' }}
|
| 102 |
whileTap={{ scale: 0.95 }}
|
| 103 |
transition={{ type: 'spring', stiffness: 400, damping: 10 }}
|
| 104 |
>
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
whileHover={{ scale: 1.05, x: 10 }}
|
| 111 |
whileTap={{ scale: 0.95 }}
|
| 112 |
>
|
| 113 |
-
<
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
<
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
| 124 |
</motion.div>
|
| 125 |
</motion.div>
|
| 126 |
<div className="hero-images">
|
|
|
|
| 1 |
import { useEffect, useRef } from 'react'
|
| 2 |
import { motion } from 'framer-motion'
|
| 3 |
import { useTranslation } from 'react-i18next'
|
| 4 |
+
import { Link } from 'react-router-dom'
|
| 5 |
import gsap from 'gsap'
|
| 6 |
import './Hero.css'
|
| 7 |
|
|
|
|
| 96 |
animate={{ opacity: 1, y: 0 }}
|
| 97 |
transition={{ delay: 1.3, duration: 0.8 }}
|
| 98 |
>
|
| 99 |
+
<motion.div
|
|
|
|
|
|
|
| 100 |
whileHover={{ scale: 1.1, y: -3, boxShadow: '0 10px 30px rgba(74, 124, 89, 0.4)' }}
|
| 101 |
whileTap={{ scale: 0.95 }}
|
| 102 |
transition={{ type: 'spring', stiffness: 400, damping: 10 }}
|
| 103 |
>
|
| 104 |
+
<Link to="/login" className="btn-primary">
|
| 105 |
+
{t('hero.cta.upload')}
|
| 106 |
+
</Link>
|
| 107 |
+
</motion.div>
|
| 108 |
+
<motion.div
|
| 109 |
whileHover={{ scale: 1.05, x: 10 }}
|
| 110 |
whileTap={{ scale: 0.95 }}
|
| 111 |
>
|
| 112 |
+
<Link to="/login" className="btn-secondary">
|
| 113 |
+
<motion.span
|
| 114 |
+
className="btn-icon"
|
| 115 |
+
whileHover={{ rotate: 90 }}
|
| 116 |
+
transition={{ type: 'spring', stiffness: 300 }}
|
| 117 |
+
>
|
| 118 |
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 119 |
+
<path d="M6 1L11 6M11 6L6 11M11 6H1" stroke="#2d2d2d" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
| 120 |
+
</svg>
|
| 121 |
+
</motion.span>
|
| 122 |
+
{t('hero.cta.howItWorks')}
|
| 123 |
+
</Link>
|
| 124 |
+
</motion.div>
|
| 125 |
</motion.div>
|
| 126 |
</motion.div>
|
| 127 |
<div className="hero-images">
|
frontend/src/pages/HomePage.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Header from '../components/Header'
|
| 2 |
+
import Hero from '../components/Hero'
|
| 3 |
+
import ServicesSection from '../components/ServicesSection'
|
| 4 |
+
import HowWeCanHelp from '../components/HowWeCanHelp'
|
| 5 |
+
import BeliefStatement from '../components/BeliefStatement'
|
| 6 |
+
import Testimonials from '../components/Testimonials'
|
| 7 |
+
import ArtOfNaturalRemedies from '../components/ArtOfNaturalRemedies'
|
| 8 |
+
import Footer from '../components/Footer'
|
| 9 |
+
|
| 10 |
+
function HomePage() {
|
| 11 |
+
return (
|
| 12 |
+
<div className="app">
|
| 13 |
+
<Header />
|
| 14 |
+
<main>
|
| 15 |
+
<Hero />
|
| 16 |
+
<ServicesSection />
|
| 17 |
+
<HowWeCanHelp />
|
| 18 |
+
<BeliefStatement />
|
| 19 |
+
<Testimonials />
|
| 20 |
+
<ArtOfNaturalRemedies />
|
| 21 |
+
</main>
|
| 22 |
+
<Footer />
|
| 23 |
+
</div>
|
| 24 |
+
)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export default HomePage
|
frontend/src/pages/Login.tsx
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { useNavigate } from 'react-router-dom'
|
| 3 |
+
import AuthShell from '../components/AuthShell'
|
| 4 |
+
import { login } from '../services/auth'
|
| 5 |
+
|
| 6 |
+
function Login() {
|
| 7 |
+
const navigate = useNavigate()
|
| 8 |
+
const [formData, setFormData] = useState({
|
| 9 |
+
email: '',
|
| 10 |
+
password: ''
|
| 11 |
+
})
|
| 12 |
+
const [error, setError] = useState('')
|
| 13 |
+
const [loading, setLoading] = useState(false)
|
| 14 |
+
|
| 15 |
+
const handleChange = (e) => {
|
| 16 |
+
setFormData({
|
| 17 |
+
...formData,
|
| 18 |
+
[e.target.name]: e.target.value
|
| 19 |
+
})
|
| 20 |
+
setError('')
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const handleSubmit = async (e) => {
|
| 24 |
+
e.preventDefault()
|
| 25 |
+
setError('')
|
| 26 |
+
setLoading(true)
|
| 27 |
+
|
| 28 |
+
try {
|
| 29 |
+
const data = await login(formData.email, formData.password)
|
| 30 |
+
localStorage.setItem('access_token', data.access_token)
|
| 31 |
+
navigate('/')
|
| 32 |
+
} catch (err) {
|
| 33 |
+
setError(err.response?.data?.detail || 'Login failed. Please check your credentials.')
|
| 34 |
+
} finally {
|
| 35 |
+
setLoading(false)
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<AuthShell
|
| 41 |
+
title="Welcome Back"
|
| 42 |
+
subtitle="Sign in to continue your health journey"
|
| 43 |
+
footerText="Don't have an account?"
|
| 44 |
+
footerLink="/signup"
|
| 45 |
+
footerLinkText="Sign up"
|
| 46 |
+
>
|
| 47 |
+
<form onSubmit={handleSubmit} className="auth-form">
|
| 48 |
+
<div className="form-group">
|
| 49 |
+
<label htmlFor="email" className="form-label">
|
| 50 |
+
Email
|
| 51 |
+
</label>
|
| 52 |
+
<input
|
| 53 |
+
id="email"
|
| 54 |
+
name="email"
|
| 55 |
+
type="email"
|
| 56 |
+
required
|
| 57 |
+
value={formData.email}
|
| 58 |
+
onChange={handleChange}
|
| 59 |
+
className="form-input"
|
| 60 |
+
placeholder="you@example.com"
|
| 61 |
+
disabled={loading}
|
| 62 |
+
/>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<div className="form-group">
|
| 66 |
+
<label htmlFor="password" className="form-label">
|
| 67 |
+
Password
|
| 68 |
+
</label>
|
| 69 |
+
<input
|
| 70 |
+
id="password"
|
| 71 |
+
name="password"
|
| 72 |
+
type="password"
|
| 73 |
+
required
|
| 74 |
+
value={formData.password}
|
| 75 |
+
onChange={handleChange}
|
| 76 |
+
className="form-input"
|
| 77 |
+
placeholder="Enter your password"
|
| 78 |
+
disabled={loading}
|
| 79 |
+
/>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
{error && <div className="form-error">{error}</div>}
|
| 83 |
+
|
| 84 |
+
<button type="submit" className="auth-button" disabled={loading}>
|
| 85 |
+
{loading ? 'Signing in...' : 'Sign In'}
|
| 86 |
+
</button>
|
| 87 |
+
</form>
|
| 88 |
+
</AuthShell>
|
| 89 |
+
)
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
export default Login
|
frontend/src/pages/Signup.tsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react'
|
| 2 |
+
import { useNavigate } from 'react-router-dom'
|
| 3 |
+
import AuthShell from '../components/AuthShell'
|
| 4 |
+
import { signup } from '../services/auth'
|
| 5 |
+
|
| 6 |
+
function Signup() {
|
| 7 |
+
const navigate = useNavigate()
|
| 8 |
+
const [formData, setFormData] = useState({
|
| 9 |
+
fullName: '',
|
| 10 |
+
email: '',
|
| 11 |
+
password: ''
|
| 12 |
+
})
|
| 13 |
+
const [error, setError] = useState('')
|
| 14 |
+
const [loading, setLoading] = useState(false)
|
| 15 |
+
|
| 16 |
+
const handleChange = (e) => {
|
| 17 |
+
setFormData({
|
| 18 |
+
...formData,
|
| 19 |
+
[e.target.name]: e.target.value
|
| 20 |
+
})
|
| 21 |
+
setError('')
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const handleSubmit = async (e) => {
|
| 25 |
+
e.preventDefault()
|
| 26 |
+
setError('')
|
| 27 |
+
setLoading(true)
|
| 28 |
+
|
| 29 |
+
try {
|
| 30 |
+
const data = await signup(formData.fullName, formData.email, formData.password)
|
| 31 |
+
localStorage.setItem('access_token', data.access_token)
|
| 32 |
+
navigate('/')
|
| 33 |
+
} catch (err) {
|
| 34 |
+
setError(err.response?.data?.detail || 'Signup failed. Please try again.')
|
| 35 |
+
} finally {
|
| 36 |
+
setLoading(false)
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
return (
|
| 41 |
+
<AuthShell
|
| 42 |
+
title="Create Account"
|
| 43 |
+
subtitle="Start your preventive health journey today"
|
| 44 |
+
footerText="Already have an account?"
|
| 45 |
+
footerLink="/login"
|
| 46 |
+
footerLinkText="Sign in"
|
| 47 |
+
>
|
| 48 |
+
<form onSubmit={handleSubmit} className="auth-form">
|
| 49 |
+
<div className="form-group">
|
| 50 |
+
<label htmlFor="fullName" className="form-label">
|
| 51 |
+
Full Name
|
| 52 |
+
</label>
|
| 53 |
+
<input
|
| 54 |
+
id="fullName"
|
| 55 |
+
name="fullName"
|
| 56 |
+
type="text"
|
| 57 |
+
required
|
| 58 |
+
value={formData.fullName}
|
| 59 |
+
onChange={handleChange}
|
| 60 |
+
className="form-input"
|
| 61 |
+
placeholder="John Doe"
|
| 62 |
+
disabled={loading}
|
| 63 |
+
/>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<div className="form-group">
|
| 67 |
+
<label htmlFor="email" className="form-label">
|
| 68 |
+
Email
|
| 69 |
+
</label>
|
| 70 |
+
<input
|
| 71 |
+
id="email"
|
| 72 |
+
name="email"
|
| 73 |
+
type="email"
|
| 74 |
+
required
|
| 75 |
+
value={formData.email}
|
| 76 |
+
onChange={handleChange}
|
| 77 |
+
className="form-input"
|
| 78 |
+
placeholder="you@example.com"
|
| 79 |
+
disabled={loading}
|
| 80 |
+
/>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div className="form-group">
|
| 84 |
+
<label htmlFor="password" className="form-label">
|
| 85 |
+
Password
|
| 86 |
+
</label>
|
| 87 |
+
<input
|
| 88 |
+
id="password"
|
| 89 |
+
name="password"
|
| 90 |
+
type="password"
|
| 91 |
+
required
|
| 92 |
+
minLength={8}
|
| 93 |
+
value={formData.password}
|
| 94 |
+
onChange={handleChange}
|
| 95 |
+
className="form-input"
|
| 96 |
+
placeholder="At least 8 characters"
|
| 97 |
+
disabled={loading}
|
| 98 |
+
/>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
{error && <div className="form-error">{error}</div>}
|
| 102 |
+
|
| 103 |
+
<button type="submit" className="auth-button" disabled={loading}>
|
| 104 |
+
{loading ? 'Creating account...' : 'Create Account'}
|
| 105 |
+
</button>
|
| 106 |
+
</form>
|
| 107 |
+
</AuthShell>
|
| 108 |
+
)
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
export default Signup
|
frontend/src/services/auth.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios from 'axios'
|
| 2 |
+
|
| 3 |
+
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
| 4 |
+
|
| 5 |
+
const api = axios.create({
|
| 6 |
+
baseURL: API_URL,
|
| 7 |
+
headers: {
|
| 8 |
+
'Content-Type': 'application/json'
|
| 9 |
+
}
|
| 10 |
+
})
|
| 11 |
+
|
| 12 |
+
api.interceptors.request.use(config => {
|
| 13 |
+
const token = localStorage.getItem('access_token')
|
| 14 |
+
if (token) {
|
| 15 |
+
config.headers.Authorization = `Bearer ${token}`
|
| 16 |
+
}
|
| 17 |
+
return config
|
| 18 |
+
})
|
| 19 |
+
|
| 20 |
+
export const signup = async (fullName, email, password) => {
|
| 21 |
+
const response = await api.post('/auth/signup', {
|
| 22 |
+
full_name: fullName,
|
| 23 |
+
email,
|
| 24 |
+
password
|
| 25 |
+
})
|
| 26 |
+
return response.data
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export const login = async (email, password) => {
|
| 30 |
+
const response = await api.post('/auth/login', {
|
| 31 |
+
email,
|
| 32 |
+
password
|
| 33 |
+
})
|
| 34 |
+
return response.data
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export const getMe = async () => {
|
| 38 |
+
const response = await api.get('/auth/me')
|
| 39 |
+
return response.data
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export const logout = () => {
|
| 43 |
+
localStorage.removeItem('access_token')
|
| 44 |
+
}
|
frontend/src/services/auth.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios from 'axios'
|
| 2 |
+
|
| 3 |
+
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
| 4 |
+
|
| 5 |
+
const api = axios.create({
|
| 6 |
+
baseURL: API_URL,
|
| 7 |
+
headers: {
|
| 8 |
+
'Content-Type': 'application/json'
|
| 9 |
+
}
|
| 10 |
+
})
|
| 11 |
+
|
| 12 |
+
api.interceptors.request.use(config => {
|
| 13 |
+
const token = localStorage.getItem('access_token')
|
| 14 |
+
if (token) {
|
| 15 |
+
config.headers.Authorization = `Bearer ${token}`
|
| 16 |
+
}
|
| 17 |
+
return config
|
| 18 |
+
})
|
| 19 |
+
|
| 20 |
+
interface SignupResponse {
|
| 21 |
+
access_token: string
|
| 22 |
+
user: {
|
| 23 |
+
id: string
|
| 24 |
+
email: string
|
| 25 |
+
full_name: string
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
interface LoginResponse {
|
| 30 |
+
access_token: string
|
| 31 |
+
user: {
|
| 32 |
+
id: string
|
| 33 |
+
email: string
|
| 34 |
+
full_name: string
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
interface MeResponse {
|
| 39 |
+
id: string
|
| 40 |
+
email: string
|
| 41 |
+
full_name: string
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export const signup = async (fullName: string, email: string, password: string): Promise<SignupResponse> => {
|
| 45 |
+
const response = await api.post<SignupResponse>('/auth/signup', {
|
| 46 |
+
full_name: fullName,
|
| 47 |
+
email,
|
| 48 |
+
password
|
| 49 |
+
})
|
| 50 |
+
return response.data
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export const login = async (email: string, password: string): Promise<LoginResponse> => {
|
| 54 |
+
const response = await api.post<LoginResponse>('/auth/login', {
|
| 55 |
+
email,
|
| 56 |
+
password
|
| 57 |
+
})
|
| 58 |
+
return response.data
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
export const getMe = async (): Promise<MeResponse> => {
|
| 62 |
+
const response = await api.get<MeResponse>('/auth/me')
|
| 63 |
+
return response.data
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export const logout = () => {
|
| 67 |
+
localStorage.removeItem('access_token')
|
| 68 |
+
}
|