darved2305 commited on
Commit
adb0589
·
1 Parent(s): c715979

backend setup in fastapi and authentication in neon for postfres

Browse files
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 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
  import './App.css'
10
 
11
  function App() {
12
  return (
13
- <div className="app">
14
- <Header />
15
- <main>
16
- <Hero />
17
- <ServicesSection />
18
- <HowWeCanHelp />
19
- <BeliefStatement />
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.a
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
- {t('header.cta')}
74
- </motion.a>
 
 
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.a
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
- {t('hero.cta.upload')}
106
- </motion.a>
107
- <motion.a
108
- href="#"
109
- className="btn-secondary"
110
  whileHover={{ scale: 1.05, x: 10 }}
111
  whileTap={{ scale: 0.95 }}
112
  >
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
- </motion.a>
 
 
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
+ }