diff --git a/.gitignore b/.gitignore
index 1262b41..8b7c2d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+# Ignore VS Code settings
+.vscode/
# Logs
logs
*.log
@@ -35,4 +37,11 @@ venv/
env/
ENV/
venv.bak/
-pycache/
\ No newline at end of file
+pycache/
+
+# Environment files
+.env
+backend/.env
+
+#next.js
+.next/
diff --git a/Backend/.env-example b/Backend/.env-example
deleted file mode 100644
index 18e42cd..0000000
--- a/Backend/.env-example
+++ /dev/null
@@ -1,10 +0,0 @@
-user=postgres
-password=[YOUR-PASSWORD]
-host=
-port=5432
-dbname=postgres
-GROQ_API_KEY=
-SUPABASE_URL=
-SUPABASE_KEY=
-GEMINI_API_KEY=
-YOUTUBE_API_KEY=
\ No newline at end of file
diff --git a/Backend/.gitignore b/Backend/.gitignore
deleted file mode 100644
index 18503aa..0000000
--- a/Backend/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-.env
-__pycache__/
-venv
-.venv
\ No newline at end of file
diff --git a/Backend/app/db/db.py b/Backend/app/db/db.py
deleted file mode 100644
index ae0f517..0000000
--- a/Backend/app/db/db.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
-from sqlalchemy.orm import sessionmaker, declarative_base
-from sqlalchemy.exc import SQLAlchemyError
-import os
-from dotenv import load_dotenv
-
-# Load environment variables from .env
-load_dotenv()
-
-# Fetch database credentials
-USER = os.getenv("user")
-PASSWORD = os.getenv("password")
-HOST = os.getenv("host")
-PORT = os.getenv("port")
-DBNAME = os.getenv("dbname")
-
-# Corrected async SQLAlchemy connection string (removed `sslmode=require`)
-DATABASE_URL = f"postgresql+asyncpg://{USER}:{PASSWORD}@{HOST}:{PORT}/{DBNAME}"
-
-# Initialize async SQLAlchemy components
-try:
- engine = create_async_engine(
- DATABASE_URL, echo=True, connect_args={"ssl": "require"}
- )
-
- AsyncSessionLocal = sessionmaker(
- bind=engine, class_=AsyncSession, expire_on_commit=False
- )
- Base = declarative_base()
- print("✅ Database connected successfully!")
-except SQLAlchemyError as e:
- print(f"❌ Error connecting to the database: {e}")
- engine = None
- AsyncSessionLocal = None
- Base = None
-
-
-async def get_db():
- async with AsyncSessionLocal() as session:
- yield session
diff --git a/Backend/app/db/seed.py b/Backend/app/db/seed.py
deleted file mode 100644
index 77a015e..0000000
--- a/Backend/app/db/seed.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from datetime import datetime
-from app.db.db import AsyncSessionLocal
-from app.models.models import User
-
-
-async def seed_db():
- users = [
- {
- "id": "aabb1fd8-ba93-4e8c-976e-35e5c40b809c",
- "username": "creator1",
- "email": "creator1@example.com",
- "password": "password123",
- "role": "creator",
- "bio": "Lifestyle and travel content creator",
- "profile_image": None,
- "created_at": datetime.utcnow()
- },
- {
- "id": "6dbfcdd5-795f-49c1-8f7a-a5538b8c6f6f",
- "username": "brand1",
- "email": "brand1@example.com",
- "password": "password123",
- "role": "brand",
- "bio": "Sustainable fashion brand looking for influencers",
- "profile_image": None,
- "created_at": datetime.utcnow()
- },
- ]
-
- # Insert or update the users
- async with AsyncSessionLocal() as session:
- for user_data in users:
- # Check if user exists
- existing_user = await session.execute(
- User.__table__.select().where(User.email == user_data["email"])
- )
- existing_user = existing_user.scalar_one_or_none()
-
- if existing_user:
- continue
- else:
- # Create new user
- user = User(
- id=user_data["id"],
- username=user_data["username"],
- email=user_data["email"],
- role=user_data["role"],
- profile_image=user_data["profile_image"],
- bio=user_data["bio"],
- created_at=user_data["created_at"]
- )
- session.add(user)
- print(f"Created user: {user_data['email']}")
-
- # Commit the session
- await session.commit()
- print("✅ Users seeded successfully.")
diff --git a/Backend/app/main.py b/Backend/app/main.py
deleted file mode 100644
index 86d892a..0000000
--- a/Backend/app/main.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from fastapi import FastAPI
-from fastapi.middleware.cors import CORSMiddleware
-from .db.db import engine
-from .db.seed import seed_db
-from .models import models, chat
-from .routes.post import router as post_router
-from .routes.chat import router as chat_router
-from .routes.match import router as match_router
-from sqlalchemy.exc import SQLAlchemyError
-import logging
-import os
-from dotenv import load_dotenv
-from contextlib import asynccontextmanager
-from app.routes import ai
-
-# Load environment variables
-load_dotenv()
-
-
-# Async function to create database tables with exception handling
-async def create_tables():
- try:
- async with engine.begin() as conn:
- await conn.run_sync(models.Base.metadata.create_all)
- await conn.run_sync(chat.Base.metadata.create_all)
- print("✅ Tables created successfully or already exist.")
- except SQLAlchemyError as e:
- print(f"❌ Error creating tables: {e}")
-
-
-# Lifespan context manager for startup and shutdown events
-@asynccontextmanager
-async def lifespan(app: FastAPI):
- print("App is starting...")
- await create_tables()
- await seed_db()
- yield
- print("App is shutting down...")
-
-
-# Initialize FastAPI
-app = FastAPI(lifespan=lifespan)
-
-# Add CORS middleware
-app.add_middleware(
- CORSMiddleware,
- allow_origins=["http://localhost:5173"],
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
-)
-
-# Include the routes
-app.include_router(post_router)
-app.include_router(chat_router)
-app.include_router(match_router)
-app.include_router(ai.router)
-app.include_router(ai.youtube_router)
-
-
-@app.get("/")
-async def home():
- try:
- return {"message": "Welcome to Inpact API!"}
- except Exception as e:
- return {"error": f"Unexpected error: {e}"}
diff --git a/Backend/app/models/chat.py b/Backend/app/models/chat.py
deleted file mode 100644
index 16c6d93..0000000
--- a/Backend/app/models/chat.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from sqlalchemy import Column, String, ForeignKey, DateTime, Enum, UniqueConstraint
-from sqlalchemy.orm import relationship
-from datetime import datetime, timezone
-from app.db.db import Base
-import uuid
-import enum
-
-
-def generate_uuid():
- return str(uuid.uuid4())
-
-
-class MessageStatus(enum.Enum):
- SENT = "sent"
- DELIVERED = "delivered"
- SEEN = "seen"
-
-
-class ChatList(Base):
- __tablename__ = "chat_list"
-
- id = Column(String, primary_key=True, default=generate_uuid)
- user1_id = Column(String, ForeignKey("users.id"), nullable=False)
- user2_id = Column(String, ForeignKey("users.id"), nullable=False)
- last_message_time = Column(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
-
- user1 = relationship("User", foreign_keys=[user1_id], backref="chatlist_user1")
- user2 = relationship("User", foreign_keys=[user2_id], backref="chatlist_user2")
-
- __table_args__ = (UniqueConstraint("user1_id", "user2_id", name="unique_chat"),)
-
-
-class ChatMessage(Base):
- __tablename__ = "chat_messages"
-
- id = Column(String, primary_key=True, default=generate_uuid)
- sender_id = Column(String, ForeignKey("users.id"), nullable=False)
- receiver_id = Column(String, ForeignKey("users.id"), nullable=False)
- message = Column(String, nullable=False)
- status = Column(
- Enum(MessageStatus), default=MessageStatus.SENT
- ) # Using the enum class
- created_at = Column(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
-
- sender = relationship("User", foreign_keys=[sender_id], backref="sent_messages")
- receiver = relationship(
- "User", foreign_keys=[receiver_id], backref="received_messages"
- )
- chat_list_id = Column(String, ForeignKey("chat_list.id"), nullable=False)
- chat = relationship("ChatList", backref="messages")
diff --git a/Backend/app/models/models.py b/Backend/app/models/models.py
deleted file mode 100644
index 56681ab..0000000
--- a/Backend/app/models/models.py
+++ /dev/null
@@ -1,162 +0,0 @@
-from sqlalchemy import (
- Column,
- String,
- Integer,
- ForeignKey,
- Float,
- Text,
- JSON,
- DECIMAL,
- DateTime,
- Boolean,
- TIMESTAMP,
-)
-from sqlalchemy.orm import relationship
-from datetime import datetime
-from app.db.db import Base
-import uuid
-
-
-def generate_uuid():
- return str(uuid.uuid4())
-
-
-# User Table (Creators & Brands)
-class User(Base):
- __tablename__ = "users"
-
- id = Column(String, primary_key=True, default=generate_uuid)
- username = Column(String, unique=True, nullable=False)
- email = Column(String, unique=True, nullable=False)
- # password_hash = Column(Text, nullable=False) # Removed as Supabase handles auth
- role = Column(String, nullable=False) # 'creator' or 'brand'
- profile_image = Column(Text, nullable=True)
- bio = Column(Text, nullable=True)
- created_at = Column(TIMESTAMP, default=datetime.utcnow)
-
- is_online = Column(Boolean, default=False) # ✅ Track if user is online
- last_seen = Column(TIMESTAMP, default=datetime.utcnow)
-
- audience = relationship("AudienceInsights", back_populates="user", uselist=False)
- sponsorships = relationship("Sponsorship", back_populates="brand")
- posts = relationship("UserPost", back_populates="user")
- applications = relationship("SponsorshipApplication", back_populates="creator")
- payments = relationship(
- "SponsorshipPayment",
- foreign_keys="[SponsorshipPayment.creator_id]",
- back_populates="creator",
- )
- brand_payments = relationship(
- "SponsorshipPayment",
- foreign_keys="[SponsorshipPayment.brand_id]",
- back_populates="brand",
- )
-
-
-# Audience Insights Table
-class AudienceInsights(Base):
- __tablename__ = "audience_insights"
-
- id = Column(String, primary_key=True, default=generate_uuid)
- user_id = Column(String, ForeignKey("users.id"), nullable=False)
- audience_age_group = Column(JSON)
- audience_location = Column(JSON)
- engagement_rate = Column(Float)
- average_views = Column(Integer)
- time_of_attention = Column(Integer) # in seconds
- price_expectation = Column(DECIMAL(10, 2))
- created_at = Column(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
-
- user = relationship("User", back_populates="audience")
-
-
-# Sponsorship Table (For Brands)
-class Sponsorship(Base):
- __tablename__ = "sponsorships"
-
- id = Column(String, primary_key=True, default=generate_uuid)
- brand_id = Column(String, ForeignKey("users.id"), nullable=False)
- title = Column(String, nullable=False)
- description = Column(Text, nullable=False)
- required_audience = Column(JSON) # {"age": ["18-24"], "location": ["USA", "UK"]}
- budget = Column(DECIMAL(10, 2))
- engagement_minimum = Column(Float)
- status = Column(String, default="open")
- created_at = Column(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
-
- brand = relationship("User", back_populates="sponsorships")
- applications = relationship("SponsorshipApplication", back_populates="sponsorship")
-
-
-# User Posts Table
-class UserPost(Base):
- __tablename__ = "user_posts"
-
- id = Column(String, primary_key=True, default=generate_uuid)
- user_id = Column(String, ForeignKey("users.id"), nullable=False)
- title = Column(String, nullable=False)
- content = Column(Text, nullable=False)
- post_url = Column(Text, nullable=True)
- category = Column(String, nullable=True)
- engagement_metrics = Column(JSON) # {"likes": 500, "comments": 100, "shares": 50}
- created_at = Column(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
-
- user = relationship("User", back_populates="posts")
-
-
-# Sponsorship Applications Table
-class SponsorshipApplication(Base):
- __tablename__ = "sponsorship_applications"
-
- id = Column(String, primary_key=True, default=generate_uuid)
- creator_id = Column(String, ForeignKey("users.id"), nullable=False)
- sponsorship_id = Column(String, ForeignKey("sponsorships.id"), nullable=False)
- post_id = Column(String, ForeignKey("user_posts.id"), nullable=True)
- proposal = Column(Text, nullable=False)
- status = Column(String, default="pending")
- applied_at = Column(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
-
- creator = relationship("User", back_populates="applications")
- sponsorship = relationship("Sponsorship", back_populates="applications")
-
-
-# Collaborations Table
-class Collaboration(Base):
- __tablename__ = "collaborations"
-
- id = Column(String, primary_key=True, default=generate_uuid)
- creator_1_id = Column(String, ForeignKey("users.id"), nullable=False)
- creator_2_id = Column(String, ForeignKey("users.id"), nullable=False)
- collaboration_details = Column(Text, nullable=False)
- status = Column(String, default="pending")
- created_at = Column(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
-
-
-# Sponsorship Payments Table
-class SponsorshipPayment(Base):
- __tablename__ = "sponsorship_payments"
-
- id = Column(String, primary_key=True, default=generate_uuid)
- creator_id = Column(String, ForeignKey("users.id"), nullable=False)
- brand_id = Column(String, ForeignKey("users.id"), nullable=False)
- sponsorship_id = Column(String, ForeignKey("sponsorships.id"), nullable=False)
- amount = Column(DECIMAL(10, 2), nullable=False)
- status = Column(String, default="pending")
- transaction_date = Column(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
-
- creator = relationship("User", foreign_keys=[creator_id], back_populates="payments")
- brand = relationship(
- "User", foreign_keys=[brand_id], back_populates="brand_payments"
- )
diff --git a/Backend/app/routes/ai.py b/Backend/app/routes/ai.py
deleted file mode 100644
index a21a482..0000000
--- a/Backend/app/routes/ai.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# FastAPI router for AI-powered endpoints, including trending niches
-from fastapi import APIRouter, HTTPException, Query
-from datetime import date
-import os
-import requests
-import json
-from supabase import create_client, Client
-from requests.adapters import HTTPAdapter
-from urllib3.util.retry import Retry
-
-# Initialize router
-router = APIRouter()
-
-# Load environment variables for Supabase and Gemini
-SUPABASE_URL = os.environ.get("SUPABASE_URL")
-SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
-GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
-
-# Validate required environment variables
-if not all([SUPABASE_URL, SUPABASE_KEY, GEMINI_API_KEY]):
- raise ValueError("Missing required environment variables: SUPABASE_URL, SUPABASE_KEY, GEMINI_API_KEY")
-
-supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
-
-def fetch_from_gemini():
- prompt = (
- "List the top 6 trending content niches for creators and brands this week. For each, provide: name (the niche), insight (a short qualitative reason why it's trending), and global_activity (a number from 1 to 5, where 5 means very high global activity in this category, and 1 means low).Return as a JSON array of objects with keys: name, insight, global_activity."
- )
- url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key={GEMINI_API_KEY}"
- # Set up retry strategy
- retry_strategy = Retry(
- total=3,
- backoff_factor=1,
- status_forcelist=[429, 500, 502, 503, 504],
- allowed_methods=["POST"],
- )
- adapter = HTTPAdapter(max_retries=retry_strategy)
- http = requests.Session()
- http.mount("https://", adapter)
- http.mount("http://", adapter)
- resp = http.post(url, json={"contents": [{"parts": [{"text": prompt}]}]}, timeout=(3.05, 10))
- resp.raise_for_status()
- print("Gemini raw response:", resp.text)
- data = resp.json()
- print("Gemini parsed JSON:", data)
- text = data['candidates'][0]['content']['parts'][0]['text']
- print("Gemini text to parse as JSON:", text)
- # Remove Markdown code block if present
- if text.strip().startswith('```'):
- text = text.strip().split('\n', 1)[1] # Remove the first line (```json)
- text = text.rsplit('```', 1)[0] # Remove the last ```
- text = text.strip()
- return json.loads(text)
-
-@router.get("/api/trending-niches")
-def trending_niches():
- """
- API endpoint to get trending niches for the current day.
- - If today's data exists in Supabase, return it.
- - Otherwise, fetch from Gemini, store in Supabase, and return the new data.
- - If Gemini fails, fallback to the most recent data available.
- """
- today = str(date.today())
- # Check if today's data exists in Supabase
- result = supabase.table("trending_niches").select("*").eq("fetched_at", today).execute()
- if not result.data:
- # Fetch from Gemini and store
- try:
- niches = fetch_from_gemini()
- for niche in niches:
- supabase.table("trending_niches").insert({
- "name": niche["name"],
- "insight": niche["insight"],
- "global_activity": int(niche["global_activity"]),
- "fetched_at": today
- }).execute()
- result = supabase.table("trending_niches").select("*").eq("fetched_at", today).execute()
- except Exception as e:
- print("Gemini fetch failed:", e)
- # fallback: serve most recent data
- result = supabase.table("trending_niches").select("*").order("fetched_at", desc=True).limit(6).execute()
- return result.data
-
-youtube_router = APIRouter(prefix="/youtube", tags=["YouTube"])
-
-@youtube_router.get("/channel-info")
-def get_youtube_channel_info(channelId: str = Query(..., description="YouTube Channel ID")):
- """
- Proxy endpoint to fetch YouTube channel info securely from the backend.
- The API key is kept secret and rate limiting can be enforced here.
- """
- api_key = os.getenv("YOUTUBE_API_KEY")
- if not api_key:
- raise HTTPException(status_code=500, detail="YouTube API key not configured on server.")
- url = f"https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&id={channelId}&key={api_key}"
- try:
- resp = requests.get(url, timeout=10)
- resp.raise_for_status()
- return resp.json()
- except requests.RequestException as e:
- raise HTTPException(status_code=502, detail=f"YouTube API error: {str(e)}")
diff --git a/Backend/app/routes/auth.py b/Backend/app/routes/auth.py
deleted file mode 100644
index 19d59a2..0000000
--- a/Backend/app/routes/auth.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from fastapi import APIRouter
-
-router = APIRouter()
-
-@router.get("/auth/ping")
-def ping():
- return {"message": "Auth route is working!"}
diff --git a/Backend/app/routes/chat.py b/Backend/app/routes/chat.py
deleted file mode 100644
index f51d6b7..0000000
--- a/Backend/app/routes/chat.py
+++ /dev/null
@@ -1,128 +0,0 @@
-from fastapi import (
- APIRouter,
- WebSocket,
- Depends,
- WebSocketDisconnect,
- Request,
- HTTPException,
-)
-from sqlalchemy.ext.asyncio import AsyncSession
-from ..db.db import get_db
-from ..services.chat_services import chat_service
-from redis.asyncio import Redis
-from ..services.redis_client import get_redis
-import asyncio
-from ..services.chat_pubsub import listen_to_channel
-
-router = APIRouter(prefix="/chat", tags=["Chat"])
-
-
-@router.websocket("/ws/{user_id}")
-async def websocket_endpoint(
- websocket: WebSocket,
- user_id: str,
- redis: Redis = Depends(get_redis),
- db: AsyncSession = Depends(get_db),
-):
- await chat_service.connect(user_id, websocket, db)
-
- listener_task = asyncio.create_task(listen_to_channel(user_id, websocket, redis))
-
- try:
- while True:
- data = await websocket.receive_json()
- event_type = data.get("event_type", "")
- if event_type == "SEND_MESSAGE":
- receiver_id = data.get("receiver_id")
- sender_id = user_id
- message_text = data.get("message")
- await chat_service.send_message(
- sender_id, receiver_id, message_text, db, redis
- )
-
- except WebSocketDisconnect:
- listener_task.cancel()
- await chat_service.disconnect(user_id, redis, db)
-
- except Exception as e:
- listener_task.cancel()
- await chat_service.disconnect(user_id, redis, db)
- # Optionally log the error
- print(f"Error in websocket for user {user_id}: {e}")
-
-
-@router.get("/user_name/{user_id}")
-async def get_user_name(user_id: str, db: AsyncSession = Depends(get_db)):
- return await chat_service.get_user_name(user_id, db)
-
-
-@router.get("/chat_list/{user_id}")
-async def get_user_chat_list(
- user_id: str,
- last_message_time: str | None = None,
- db: AsyncSession = Depends(get_db),
-):
- return await chat_service.get_user_chat_list(user_id, last_message_time, db)
-
-
-@router.get("/user_status/{target_user_id}")
-async def get_user_status(
- target_user_id: str,
- redis: Redis = Depends(get_redis),
- db: AsyncSession = Depends(get_db),
-):
- return await chat_service.get_user_status(target_user_id, redis, db)
-
-
-@router.get("/messages/{user_id}/{chat_list_id}")
-async def get_chat_history(
- user_id: str,
- chat_list_id: str,
- last_fetched: int = 0,
- db: AsyncSession = Depends(get_db),
-):
- return await chat_service.get_chat_history(user_id, chat_list_id, last_fetched, db)
-
-
-@router.put("/read/{user_id}/{chat_list_id}/{message_id}")
-async def mark_message_as_read(
- user_id: str,
- chat_list_id: str,
- message_id: str,
- db: AsyncSession = Depends(get_db),
- redis: Redis = Depends(get_redis),
-):
- if not message_id:
- raise HTTPException(status_code=400, detail="message_id is required")
-
- return await chat_service.mark_message_as_read(
- user_id, chat_list_id, message_id, db, redis
- )
-
-
-@router.put("/read/{user_id}/{chat_list_id}")
-async def mark_chat_as_read(
- user_id: str,
- chat_list_id: str,
- db: AsyncSession = Depends(get_db),
- redis: Redis = Depends(get_redis),
-):
- if not chat_list_id:
- raise HTTPException(status_code=400, detail="chat_list_id is required")
-
- return await chat_service.mark_chat_as_read(user_id, chat_list_id, db, redis)
-
-
-@router.post("/new_chat/{user_id}/{username}")
-async def create_new_chat_message(
- user_id: str,
- username: str,
- request: Request,
- db: AsyncSession = Depends(get_db),
- redis: Redis = Depends(get_redis),
-):
- body = await request.json()
- message = body.get("message")
- return await chat_service.create_new_chat_message(
- user_id, username, message, db, redis
- )
diff --git a/Backend/app/routes/match.py b/Backend/app/routes/match.py
deleted file mode 100644
index 48ba7f5..0000000
--- a/Backend/app/routes/match.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from fastapi import APIRouter, HTTPException
-from supabase import create_client, Client
-import os
-from dotenv import load_dotenv
-from ..services.db_service import match_creators_for_brand, match_brands_for_creator
-
-# Load environment variables
-# load_dotenv()
-# url: str = os.getenv("SUPABASE_URL")
-# key: str = os.getenv("SUPABASE_KEY")
-# supabase: Client = create_client(url, key)
-
-router = APIRouter(prefix="/match", tags=["Matching"])
-
-@router.get("/creators-for-brand/{sponsorship_id}")
-def get_creators_for_brand(sponsorship_id: str):
- matches = match_creators_for_brand(sponsorship_id)
- if not matches:
- raise HTTPException(status_code=404, detail="No matching creators found.")
- return {"matches": matches}
-
-@router.get("/brands-for-creator/{creator_id}")
-def get_brands_for_creator(creator_id: str):
- matches = match_brands_for_creator(creator_id)
- if not matches:
- raise HTTPException(status_code=404, detail="No matching brand campaigns found.")
- return {"matches": matches}
-
-# Placeholder for endpoints, logic to be added next
\ No newline at end of file
diff --git a/Backend/app/routes/post.py b/Backend/app/routes/post.py
deleted file mode 100644
index a90e313..0000000
--- a/Backend/app/routes/post.py
+++ /dev/null
@@ -1,199 +0,0 @@
-from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.future import select
-from ..db.db import AsyncSessionLocal
-from ..models.models import (
- User, AudienceInsights, Sponsorship, UserPost,
- SponsorshipApplication, SponsorshipPayment, Collaboration
-)
-from ..schemas.schema import (
- UserCreate, AudienceInsightsCreate, SponsorshipCreate, UserPostCreate,
- SponsorshipApplicationCreate, SponsorshipPaymentCreate, CollaborationCreate
-)
-
-from fastapi import APIRouter, HTTPException
-import os
-from supabase import create_client, Client
-from dotenv import load_dotenv
-import uuid
-from datetime import datetime, timezone
-
-# Load environment variables
-load_dotenv()
-url: str = os.getenv("SUPABASE_URL")
-key: str = os.getenv("SUPABASE_KEY")
-supabase: Client = create_client(url, key)
-
-# Define Router
-router = APIRouter()
-
-# Helper Functions
-def generate_uuid():
- return str(uuid.uuid4())
-
-def current_timestamp():
- return datetime.now(timezone.utc).isoformat()
-
-# ========== USER ROUTES ==========
-@router.post("/users/")
-async def create_user(user: UserCreate):
- user_id = generate_uuid()
- t = current_timestamp()
-
- response = supabase.table("users").insert({
- "id": user_id,
- "username": user.username,
- "email": user.email,
- "role": user.role,
- "profile_image": user.profile_image,
- "bio": user.bio,
- "created_at": t
- }).execute()
-
- return response
-
-@router.get("/users/")
-async def get_users():
- result = supabase.table("users").select("*").execute()
- return result
-
-# ========== AUDIENCE INSIGHTS ROUTES ==========
-@router.post("/audience-insights/")
-async def create_audience_insights(insights: AudienceInsightsCreate):
- insight_id = generate_uuid()
- t = current_timestamp()
-
- response = supabase.table("audience_insights").insert({
- "id": insight_id,
- "user_id": insights.user_id,
- "audience_age_group": insights.audience_age_group,
- "audience_location": insights.audience_location,
- "engagement_rate": insights.engagement_rate,
- "average_views": insights.average_views,
- "time_of_attention": insights.time_of_attention,
- "price_expectation": insights.price_expectation,
- "created_at": t
- }).execute()
-
- return response
-
-@router.get("/audience-insights/")
-async def get_audience_insights():
- result = supabase.table("audience_insights").select("*").execute()
- return result
-
-# ========== SPONSORSHIP ROUTES ==========
-@router.post("/sponsorships/")
-async def create_sponsorship(sponsorship: SponsorshipCreate):
- sponsorship_id = generate_uuid()
- t = current_timestamp()
-
- response = supabase.table("sponsorships").insert({
- "id": sponsorship_id,
- "brand_id": sponsorship.brand_id,
- "title": sponsorship.title,
- "description": sponsorship.description,
- "required_audience": sponsorship.required_audience,
- "budget": sponsorship.budget,
- "engagement_minimum": sponsorship.engagement_minimum,
- "status": sponsorship.status,
- "created_at": t
- }).execute()
-
- return response
-
-@router.get("/sponsorships/")
-async def get_sponsorships():
- result = supabase.table("sponsorships").select("*").execute()
- return result
-
-# ========== USER POST ROUTES ==========
-@router.post("/posts/")
-async def create_post(post: UserPostCreate):
- post_id = generate_uuid()
- t = current_timestamp()
-
- response = supabase.table("user_posts").insert({
- "id": post_id,
- "user_id": post.user_id,
- "title": post.title,
- "content": post.content,
- "post_url": post.post_url,
- "category": post.category,
- "engagement_metrics": post.engagement_metrics,
- "created_at": t
- }).execute()
-
- return response
-
-@router.get("/posts/")
-async def get_posts():
- result = supabase.table("user_posts").select("*").execute()
- return result
-
-# ========== SPONSORSHIP APPLICATION ROUTES ==========
-@router.post("/sponsorship-applications/")
-async def create_sponsorship_application(application: SponsorshipApplicationCreate):
- application_id = generate_uuid()
- t = current_timestamp()
-
- response = supabase.table("sponsorship_applications").insert({
- "id": application_id,
- "creator_id": application.creator_id,
- "sponsorship_id": application.sponsorship_id,
- "post_id": application.post_id,
- "proposal": application.proposal,
- "status": application.status,
- "applied_at": t
- }).execute()
-
- return response
-
-@router.get("/sponsorship-applications/")
-async def get_sponsorship_applications():
- result = supabase.table("sponsorship_applications").select("*").execute()
- return result
-
-# ========== SPONSORSHIP PAYMENT ROUTES ==========
-@router.post("/sponsorship-payments/")
-async def create_sponsorship_payment(payment: SponsorshipPaymentCreate):
- payment_id = generate_uuid()
- t = current_timestamp()
-
- response = supabase.table("sponsorship_payments").insert({
- "id": payment_id,
- "creator_id": payment.creator_id,
- "sponsorship_id": payment.sponsorship_id,
- "amount": payment.amount,
- "status": payment.status,
- "payment_date": t
- }).execute()
-
- return response
-
-@router.get("/sponsorship-payments/")
-async def get_sponsorship_payments():
- result = supabase.table("sponsorship_payments").select("*").execute()
- return result
-
-# ========== COLLABORATION ROUTES ==========
-@router.post("/collaborations/")
-async def create_collaboration(collab: CollaborationCreate):
- collaboration_id = generate_uuid()
- t = current_timestamp()
-
- response = supabase.table("collaborations").insert({
- "id": collaboration_id,
- "creator_1_id": collab.creator_1_id,
- "creator_2_id": collab.creator_2_id,
- "collab_details": collab.collab_details,
- "status": collab.status,
- "created_at": t
- }).execute()
-
- return response
-
-@router.get("/collaborations/")
-async def get_collaborations():
- result = supabase.table("collaborations").select("*").execute()
- return result
diff --git a/Backend/app/schemas/schema.py b/Backend/app/schemas/schema.py
deleted file mode 100644
index 7389488..0000000
--- a/Backend/app/schemas/schema.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from pydantic import BaseModel
-from typing import Optional, Dict
-from datetime import datetime
-
-class UserCreate(BaseModel):
- username: str
- email: str
- role: str
- profile_image: Optional[str] = None
- bio: Optional[str] = None
-
-class AudienceInsightsCreate(BaseModel):
- user_id: str
- audience_age_group: Dict[str, int]
- audience_location: Dict[str, int]
- engagement_rate: float
- average_views: int
- time_of_attention: int
- price_expectation: float
-
-class SponsorshipCreate(BaseModel):
- brand_id: str
- title: str
- description: str
- required_audience: Dict[str, list]
- budget: float
- engagement_minimum: float
-
-class UserPostCreate(BaseModel):
- user_id: str
- title: str
- content: str
- post_url: Optional[str] = None
- category: Optional[str] = None
- engagement_metrics: Dict[str, int]
-
-class SponsorshipApplicationCreate(BaseModel):
- creator_id: str
- sponsorship_id: str
- post_id: Optional[str] = None
- proposal: str
-
-class SponsorshipPaymentCreate(BaseModel):
- creator_id: str
- brand_id: str
- sponsorship_id: str
- amount: float
- status: Optional[str] = "pending"
-
-class CollaborationCreate(BaseModel):
- creator_1_id: str
- creator_2_id: str
- collaboration_details: str
diff --git a/Backend/app/services/ai_services.py b/Backend/app/services/ai_services.py
deleted file mode 100644
index 30482d3..0000000
--- a/Backend/app/services/ai_services.py
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-# from sqlalchemy.pool import NullPool
-from dotenv import load_dotenv
-import os
-
-# Load environment variables from .env
-load_dotenv()
-
-# ChatGroq API keys
-CHATGROQ_API_URL_TRANSCRIBE = "https://api.groq.com/openai/v1/audio/transcriptions"
-CHATGROQ_API_URL_CHAT = "https://api.groq.com/openai/v1/chat/completions"
-API_KEY = os.getenv("GROQ_API_KEY")
-
-import requests
-
-def query_sponsorship_client(info):
- prompt = f"Extract key details about sponsorship and client interactions from the following:\n\n{info}\n\nRespond in JSON with 'sponsorship_details' and 'client_interaction_summary'."
-
- headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
- payload = {"model": "llama3-8b-8192", "messages": [{"role": "user", "content": prompt}], "temperature": 0}
-
- try:
- response = requests.post(CHATGROQ_API_URL_CHAT, json=payload, headers=headers)
- return response.json().get("choices", [{}])[0].get("message", {}).get("content", {})
- except Exception as e:
- return {"error": str(e)}
diff --git a/Backend/app/services/chat_pubsub.py b/Backend/app/services/chat_pubsub.py
deleted file mode 100644
index 1b9e8cd..0000000
--- a/Backend/app/services/chat_pubsub.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from fastapi import WebSocket
-from redis.asyncio import Redis
-import json
-
-
-async def listen_to_channel(user_id: str, websocket: WebSocket, redis_client: Redis):
- pubsub = redis_client.pubsub()
- await pubsub.subscribe(f"to_user:{user_id}")
-
- try:
- async for message in pubsub.listen():
- if message["type"] == "message":
- await websocket.send_json(json.loads(message["data"]))
- finally:
- await pubsub.unsubscribe(f"to_user:{user_id}")
- await pubsub.close()
diff --git a/Backend/app/services/chat_services.py b/Backend/app/services/chat_services.py
deleted file mode 100644
index 4b5d1a6..0000000
--- a/Backend/app/services/chat_services.py
+++ /dev/null
@@ -1,428 +0,0 @@
-from fastapi import WebSocket, HTTPException
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.sql import select
-from datetime import datetime, timezone
-from app.models.models import User
-from app.models.chat import ChatList, ChatMessage, MessageStatus
-from typing import Dict
-from redis.asyncio import Redis
-import logging
-import json
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-
-class ChatService:
- def __init__(self):
- self.active_connections: Dict[str, WebSocket] = {}
-
- async def connect(
- self,
- user_id: str,
- websocket: WebSocket,
- db: AsyncSession,
- ):
- """Accept WebSocket connection and update user status to online."""
- await websocket.accept()
- # Mark user as online
- user = await db.get(User, user_id)
- if user:
- self.active_connections[user_id] = websocket
- user.is_online = True
- await db.commit()
-
- query = select(ChatMessage).where(
- (ChatMessage.receiver_id == user_id)
- & (ChatMessage.status == MessageStatus.SENT)
- )
- messages = (await db.execute(query)).scalars().all()
- # mark as delivered
- for message in messages:
- message.status = MessageStatus.DELIVERED
- await db.commit()
- else:
- logger.warning(f"User {user_id} not found in the database.")
- await websocket.close()
-
- async def disconnect(self, user_id: str, redis: Redis, db: AsyncSession):
- """Remove connection and update last seen."""
- self.active_connections.pop(user_id, None)
-
- # Mark user as offline and update last seen
- user = await db.get(User, user_id)
- if user:
- user.is_online = False
- user.last_seen = datetime.now(timezone.utc)
- await db.commit()
- await redis.set(
- f"user:{user_id}:last_seen", user.last_seen.isoformat(), ex=600
- )
-
- async def send_message(
- self,
- sender_id: str,
- receiver_id: str,
- message_text: str,
- db: AsyncSession,
- redis: Redis,
- ):
- """Send a message to the receiver if they are online."""
-
- if not message_text:
- raise HTTPException(status_code=400, detail="Message text is required")
- if sender_id == receiver_id:
- raise HTTPException(
- status_code=400, detail="Cannot send message to yourself"
- )
-
- # Find or create chat list
- chat_list = await db.execute(
- select(ChatList).where(
- ((ChatList.user1_id == sender_id) & (ChatList.user2_id == receiver_id))
- | (
- (ChatList.user1_id == receiver_id)
- & (ChatList.user2_id == sender_id)
- )
- )
- )
- chat_list = chat_list.scalar_one_or_none()
-
- is_chat_list_exists = chat_list is not None
-
- if not chat_list:
- chat_list = ChatList(user1_id=sender_id, user2_id=receiver_id)
- db.add(chat_list)
- await db.commit()
-
- # Store message in DB
- new_message = ChatMessage(
- sender_id=sender_id,
- receiver_id=receiver_id,
- chat_list_id=chat_list.id,
- message=message_text,
- status=MessageStatus.SENT,
- )
- db.add(new_message)
- await db.commit()
-
- # Update last message time
- chat_list.last_message_time = datetime.now(timezone.utc)
- await db.commit()
-
- receiver_channel = f"to_user:{receiver_id}"
- sender_channel = f"to_user:{sender_id}"
-
- # Send message to receiver if online
- if receiver_id in self.active_connections:
- new_message.status = MessageStatus.DELIVERED
- await db.commit()
-
- if sender_id in self.active_connections:
- await redis.publish(
- sender_channel,
- json.dumps(
- {
- "eventType": "NEW_MESSAGE_DELIVERED",
- "chatListId": chat_list.id,
- "id": new_message.id,
- "message": message_text,
- "createdAt": new_message.created_at.isoformat(),
- "isSent": True,
- "status": "delivered",
- "senderId": sender_id,
- }
- ),
- )
-
- # Send message to receiver
- await redis.publish(
- receiver_channel,
- json.dumps(
- {
- "eventType": "NEW_MESSAGE_RECEIVED",
- "chatListId": chat_list.id,
- "id": new_message.id,
- "message": message_text,
- "createdAt": new_message.created_at.isoformat(),
- "isSent": False,
- "senderId": sender_id,
- }
- ),
- )
-
- else:
- if sender_id in self.active_connections:
- # Send delivered message to sender
- await redis.publish(
- sender_channel,
- json.dumps(
- {
- "eventType": "NEW_MESSAGE_SENT",
- "chatListId": chat_list.id,
- "id": new_message.id,
- "message": message_text,
- "createdAt": new_message.created_at.isoformat(),
- "isSent": True,
- "status": "sent",
- "senderId": receiver_id,
- }
- ),
- )
-
- # used in create_new_chat_message
- return {
- "chatListId": chat_list.id,
- "isChatListExists": is_chat_list_exists,
- }
-
- async def get_chat_history(
- self, user_id: str, chat_list_id: str, last_fetched: int, db: AsyncSession
- ):
- """Fetch chat history between two users."""
- limit = 20
- last_fetched_date = (
- datetime.fromtimestamp(last_fetched / 1000, tz=timezone.utc)
- if last_fetched
- else datetime.now(timezone.utc)
- )
- chat_list = await db.execute(
- select(ChatList).where(
- ((ChatList.user1_id == user_id) | (ChatList.user2_id == user_id))
- & (ChatList.id == chat_list_id)
- )
- )
- chat_list = chat_list.scalar_one_or_none()
-
- if not chat_list:
- raise HTTPException(status_code=404, detail="Chat not found")
-
- messages = await db.execute(
- select(ChatMessage)
- .where(
- ChatMessage.chat_list_id == chat_list.id,
- ChatMessage.created_at < last_fetched_date,
- )
- .order_by(ChatMessage.created_at.desc())
- .limit(limit)
- )
- messages = messages.scalars().all()
- # Format messages removing user IDs and adding isSent flag
- formatted_messages = []
- for message in messages:
- formatted_message = {
- "id": message.id,
- "message": message.message,
- "status": message.status.value,
- "createdAt": message.created_at.isoformat(),
- "isSent": message.sender_id == user_id,
- }
- formatted_messages.append(formatted_message)
-
- return formatted_messages
-
- async def mark_message_as_read(
- self,
- user_id: str,
- chat_list_id: str,
- message_id: str,
- db: AsyncSession,
- redis: Redis,
- ):
- """Mark a specific message as read and notify sender."""
- # Get the specific message
- message = await db.get(ChatMessage, message_id)
-
- if not message:
- raise HTTPException(status_code=404, detail="Message not found")
-
- # Verify the message belongs to the specified chat list and user is the receiver
- if message.chat_list_id != chat_list_id or message.receiver_id != user_id:
- raise HTTPException(
- status_code=403, detail="Not authorized to mark this message as read"
- )
-
- # Update message status
- if message.status != MessageStatus.SEEN:
- message.status = MessageStatus.SEEN
- await db.commit()
-
- # Notify sender if they're online
- if message.sender_id in self.active_connections:
- # Send message read notification to sender
- await redis.publish(
- f"to_user:{message.sender_id}",
- json.dumps(
- {
- "eventType": "MESSAGE_READ",
- "chatListId": chat_list_id,
- "messageId": message_id,
- }
- ),
- )
-
- return True
-
- async def mark_chat_as_read(
- self, user_id: str, chat_list_id: str, db: AsyncSession, redis: Redis
- ):
- """Mark messages as read and notify sender."""
- result = await db.execute(
- select(ChatMessage).where(
- (
- (ChatMessage.sender_id == user_id)
- | (ChatMessage.receiver_id == user_id)
- )
- & (ChatMessage.chat_list_id == chat_list_id)
- & (ChatMessage.status != MessageStatus.SEEN)
- )
- )
- messages = result.scalars().all()
-
- for message in messages:
- if message.sender_id == user_id:
- message.status = MessageStatus.SEEN
-
- await db.commit()
-
- receiver_id = (
- (
- messages[0].receiver_id
- if messages[0].sender_id == user_id
- else messages[0].sender_id
- )
- if len(messages)
- else None
- )
-
- # Notify receiver
- if receiver_id and (receiver_id in self.active_connections):
- await redis.publish(
- f"to_user:{receiver_id}",
- json.dumps(
- {
- "eventType": "CHAT_MESSAGES_READ",
- "chatListId": chat_list_id,
- }
- ),
- )
-
- return {"message": "Messages marked as read"}
-
- async def get_user_status(
- self, target_user_id: str, redis: Redis, db: AsyncSession
- ):
- """Check if user is online. If not, send their last seen time."""
- is_online = target_user_id in self.active_connections
- if not is_online:
- last_seen = await redis.get(f"user:{target_user_id}:last_seen")
- if not last_seen:
- user = await db.get(User, target_user_id)
- if user:
- last_seen = user.last_seen
- await redis.set(
- f"user:{target_user_id}:last_seen",
- last_seen.isoformat(),
- ex=600,
- )
- return {
- "isOnline": False,
- "lastSeen": last_seen,
- }
-
- return {
- "isOnline": is_online,
- }
-
- async def get_user_chat_list(
- self, user_id: str, last_message_time: str | None, db: AsyncSession
- ):
- """Get all chat lists for a user."""
- limit = 20
- last_message_date = (
- datetime.fromisoformat(last_message_time)
- if last_message_time
- else datetime.now(timezone.utc)
- )
- chat_lists = await db.execute(
- select(ChatList)
- .where(
- ((ChatList.user1_id == user_id) | (ChatList.user2_id == user_id))
- & (ChatList.last_message_time < last_message_date)
- )
- .order_by(ChatList.last_message_time.desc())
- .limit(limit)
- )
- chat_lists = chat_lists.scalars().all()
-
- formatted_chat_lists = []
- for chat_list in chat_lists:
- receiver_id = (
- chat_list.user1_id
- if chat_list.user2_id == user_id
- else chat_list.user2_id
- )
-
- receiver = await db.get(User, receiver_id)
- if not receiver:
- continue
- formatted_chat_list = {
- "chatListId": chat_list.id,
- "lastMessageTime": chat_list.last_message_time.isoformat(),
- "receiver": {
- "id": receiver.id,
- "username": receiver.username,
- "profileImage": receiver.profile_image,
- },
- }
- formatted_chat_lists.append(formatted_chat_list)
- return formatted_chat_lists
-
- async def get_user_name(self, user_id: str, db: AsyncSession):
- """Get the username of a user."""
- user = await db.get(User, user_id)
- if not user:
- raise HTTPException(status_code=404, detail="User not found")
- return {
- "username": user.username,
- "profileImage": user.profile_image,
- }
-
- async def create_new_chat_message(
- self,
- user_id: str,
- username: str,
- message_text: str,
- db: AsyncSession,
- redis: Redis,
- ):
- """Create a new chat message."""
- if not message_text:
- raise HTTPException(status_code=400, detail="Message text is required")
-
- receiver = await db.execute(select(User).where(User.username == username))
-
- receiver = receiver.scalar_one_or_none()
- if not receiver:
- raise HTTPException(status_code=404, detail="Receiver not found")
- if receiver.id == user_id:
- raise HTTPException(
- status_code=400, detail="Cannot send message to yourself"
- )
-
- chat_list = await db.execute(
- select(ChatList).where(
- ((ChatList.user1_id == user_id) & (ChatList.user2_id == receiver.id))
- | ((ChatList.user1_id == receiver.id) & (ChatList.user2_id == user_id))
- )
- )
- if chat_list:
- return {
- "chatListId": chat_list.scalar_one().id,
- "isChatListExists": True,
- }
-
- return await self.send_message(user_id, receiver.id, message_text, db, redis)
-
-
-chat_service = ChatService()
diff --git a/Backend/app/services/db_service.py b/Backend/app/services/db_service.py
deleted file mode 100644
index ccb4199..0000000
--- a/Backend/app/services/db_service.py
+++ /dev/null
@@ -1,85 +0,0 @@
-from supabase import create_client, Client
-import os
-from dotenv import load_dotenv
-from typing import List, Dict, Any
-
-# Load environment variables
-load_dotenv()
-url: str = os.getenv("SUPABASE_URL")
-key: str = os.getenv("SUPABASE_KEY")
-supabase: Client = create_client(url, key)
-
-
-def match_creators_for_brand(sponsorship_id: str) -> List[Dict[str, Any]]:
- # Fetch sponsorship details
- sponsorship_resp = supabase.table("sponsorships").select("*").eq("id", sponsorship_id).execute()
- if not sponsorship_resp.data:
- return []
- sponsorship = sponsorship_resp.data[0]
-
- # Fetch all audience insights (for creators)
- audience_resp = supabase.table("audience_insights").select("*").execute()
- creators = []
- for audience in audience_resp.data:
- # Basic matching logic: audience, engagement, price, etc.
- match_score = 0
- # Audience age group overlap
- if 'required_audience' in sponsorship and 'audience_age_group' in audience:
- required_ages = sponsorship['required_audience'].get('age_group', [])
- creator_ages = audience.get('audience_age_group', {})
- overlap = sum([creator_ages.get(age, 0) for age in required_ages])
- if overlap > 0:
- match_score += 1
- # Audience location overlap
- if 'required_audience' in sponsorship and 'audience_location' in audience:
- required_locs = sponsorship['required_audience'].get('location', [])
- creator_locs = audience.get('audience_location', {})
- overlap = sum([creator_locs.get(loc, 0) for loc in required_locs])
- if overlap > 0:
- match_score += 1
- # Engagement rate
- if audience.get('engagement_rate', 0) >= sponsorship.get('engagement_minimum', 0):
- match_score += 1
- # Price expectation
- if audience.get('price_expectation', 0) <= sponsorship.get('budget', 0):
- match_score += 1
- if match_score >= 2: # Threshold for a match
- creators.append({"user_id": audience["user_id"], "match_score": match_score, **audience})
- return creators
-
-
-def match_brands_for_creator(creator_id: str) -> List[Dict[str, Any]]:
- # Fetch creator's audience insights
- audience_resp = supabase.table("audience_insights").select("*").eq("user_id", creator_id).execute()
- if not audience_resp.data:
- return []
- audience = audience_resp.data[0]
-
- # Fetch all sponsorships
- sponsorships_resp = supabase.table("sponsorships").select("*").execute()
- matches = []
- for sponsorship in sponsorships_resp.data:
- match_score = 0
- # Audience age group overlap
- if 'required_audience' in sponsorship and 'audience_age_group' in audience:
- required_ages = sponsorship['required_audience'].get('age_group', [])
- creator_ages = audience.get('audience_age_group', {})
- overlap = sum([creator_ages.get(age, 0) for age in required_ages])
- if overlap > 0:
- match_score += 1
- # Audience location overlap
- if 'required_audience' in sponsorship and 'audience_location' in audience:
- required_locs = sponsorship['required_audience'].get('location', [])
- creator_locs = audience.get('audience_location', {})
- overlap = sum([creator_locs.get(loc, 0) for loc in required_locs])
- if overlap > 0:
- match_score += 1
- # Engagement rate
- if audience.get('engagement_rate', 0) >= sponsorship.get('engagement_minimum', 0):
- match_score += 1
- # Price expectation
- if audience.get('price_expectation', 0) <= sponsorship.get('budget', 0):
- match_score += 1
- if match_score >= 2: # Threshold for a match
- matches.append({"sponsorship_id": sponsorship["id"], "match_score": match_score, **sponsorship})
- return matches
diff --git a/Backend/app/services/redis_client.py b/Backend/app/services/redis_client.py
deleted file mode 100644
index d2fb922..0000000
--- a/Backend/app/services/redis_client.py
+++ /dev/null
@@ -1,7 +0,0 @@
-import redis.asyncio as redis
-
-redis_client = redis.Redis(host="localhost", port=6379, decode_responses=True)
-
-
-async def get_redis():
- return redis_client
diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml
deleted file mode 100644
index aa1451b..0000000
--- a/Backend/docker-compose.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-services:
- redis:
- image: redis:latest
- container_name: redis
- ports:
- - "6379:6379"
- volumes:
- - redis_data:/data
- restart: unless-stopped
- command: redis-server --appendonly yes
-
-volumes:
- redis_data:
diff --git a/Backend/requirements.txt b/Backend/requirements.txt
deleted file mode 100644
index ea1ab73..0000000
--- a/Backend/requirements.txt
+++ /dev/null
@@ -1,55 +0,0 @@
-aiohappyeyeballs==2.6.1
-aiohttp==3.11.12
-aiosignal==1.3.2
-alembic==1.15.2
-annotated-types==0.7.0
-anyio==4.9.0
-asyncpg==0.30.0
-attrs==25.3.0
-certifi==2025.1.31
-charset-normalizer==3.4.1
-click==8.1.8
-deprecation==2.1.0
-fastapi==0.115.12
-frozenlist==1.5.0
-gotrue==2.12.0
-greenlet==3.1.1
-h11==0.14.0
-h2==4.2.0
-hpack==4.1.0
-httpcore==1.0.7
-httpx==0.28.1
-hyperframe==6.1.0
-idna==3.10
-iniconfig==2.1.0
-Mako==1.3.9
-MarkupSafe==3.0.2
-multidict==6.3.0
-packaging==24.2
-pluggy==1.5.0
-postgrest==1.0.1
-propcache==0.3.1
-pydantic==2.11.1
-pydantic_core==2.33.0
-PyJWT==2.10.1
-pytest==8.3.5
-pytest-mock==3.14.0
-python-dateutil==2.9.0.post0
-python-dotenv==1.1.0
-realtime==2.4.0
-redis==5.2.1
-requests==2.32.3
-six==1.17.0
-sniffio==1.3.1
-SQLAlchemy==2.0.40
-starlette==0.46.1
-storage3==0.11.3
-StrEnum==0.4.15
-supabase==2.15.0
-supafunc==0.9.4
-typing-inspection==0.4.0
-typing_extensions==4.13.0
-urllib3==2.3.0
-uvicorn==0.34.0
-websockets==14.2
-yarl==1.18.3
diff --git a/Backend/sql.txt b/Backend/sql.txt
deleted file mode 100644
index 3ee28b5..0000000
--- a/Backend/sql.txt
+++ /dev/null
@@ -1,41 +0,0 @@
--- Insert into users table
-INSERT INTO users (id, username, email, role, profile_image, bio, created_at) VALUES
- (gen_random_uuid(), 'creator1', 'creator1@example.com', 'creator', 'image1.jpg', 'Bio of creator1', NOW()),
- (gen_random_uuid(), 'brand1', 'brand1@example.com', 'brand', 'image2.jpg', 'Bio of brand1', NOW()),
- (gen_random_uuid(), 'creator2', 'creator2@example.com', 'creator', 'image3.jpg', 'Bio of creator2', NOW());
-
--- Insert into audience_insights table
-INSERT INTO audience_insights (id, user_id, audience_age_group, audience_location, engagement_rate, average_views, time_of_attention, price_expectation, created_at) VALUES
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), '{"18-24": 70, "25-34": 30}', '{"USA": 50, "UK": 50}', 4.5, 10000, 120, 500.00, NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator2'), '{"18-24": 60, "25-34": 40}', '{"India": 70, "Canada": 30}', 3.8, 8000, 100, 450.00, NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'brand1'), '{"18-24": 50, "25-34": 50}', '{"Germany": 60, "France": 40}', 4.2, 9000, 110, 480.00, NOW());
-
--- Insert into sponsorships table
-INSERT INTO sponsorships (id, brand_id, title, description, required_audience, budget, engagement_minimum, status, created_at) VALUES
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'brand1'), 'Tech Sponsorship', 'Sponsorship for tech influencers', '{"age": ["18-24"], "location": ["USA", "UK"]}', 5000.00, 4.0, 'open', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'brand1'), 'Fashion Sponsorship', 'Sponsorship for fashion bloggers', '{"age": ["18-34"], "location": ["India"]}', 3000.00, 3.5, 'open', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'brand1'), 'Gaming Sponsorship', 'Sponsorship for gaming content creators', '{"age": ["18-30"], "location": ["Germany"]}', 4000.00, 4.2, 'open', NOW());
-
--- Insert into user_posts table
-INSERT INTO user_posts (id, user_id, title, content, post_url, category, engagement_metrics, created_at) VALUES
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), 'Tech Review', 'A review of the latest smartphone.', 'https://example.com/post1', 'Tech', '{"likes": 500, "comments": 100, "shares": 50}', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator2'), 'Fashion Trends', 'Exploring the latest fashion trends.', 'https://example.com/post2', 'Fashion', '{"likes": 300, "comments": 50, "shares": 20}', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), 'Gaming Setup', 'A detailed guide on the best gaming setup.', 'https://example.com/post3', 'Gaming', '{"likes": 400, "comments": 80, "shares": 40}', NOW());
-
--- Insert into sponsorship_applications table
-INSERT INTO sponsorship_applications (id, creator_id, sponsorship_id, post_id, proposal, status, applied_at) VALUES
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), (SELECT id FROM sponsorships WHERE title = 'Tech Sponsorship'), (SELECT id FROM user_posts WHERE title = 'Tech Review'), 'I am interested in this sponsorship', 'pending', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator2'), (SELECT id FROM sponsorships WHERE title = 'Fashion Sponsorship'), (SELECT id FROM user_posts WHERE title = 'Fashion Trends'), 'I can provide quality content', 'pending', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), (SELECT id FROM sponsorships WHERE title = 'Gaming Sponsorship'), (SELECT id FROM user_posts WHERE title = 'Gaming Setup'), 'I am a perfect fit for this campaign', 'pending', NOW());
-
--- Insert into collaborations table
-INSERT INTO collaborations (id, creator_1_id, creator_2_id, collaboration_details, status, created_at) VALUES
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), (SELECT id FROM users WHERE username = 'creator2'), 'Collaboration on tech and fashion', 'pending', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator2'), (SELECT id FROM users WHERE username = 'creator1'), 'Gaming and tech collaboration', 'pending', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), (SELECT id FROM users WHERE username = 'brand1'), 'Brand deal collaboration', 'pending', NOW());
-
--- Insert into sponsorship_payments table
-INSERT INTO sponsorship_payments (id, creator_id, brand_id, sponsorship_id, amount, status, transaction_date) VALUES
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), (SELECT id FROM users WHERE username = 'brand1'), (SELECT id FROM sponsorships WHERE title = 'Tech Sponsorship'), 500.00, 'completed', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator2'), (SELECT id FROM users WHERE username = 'brand1'), (SELECT id FROM sponsorships WHERE title = 'Fashion Sponsorship'), 300.00, 'completed', NOW()),
- (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), (SELECT id FROM users WHERE username = 'brand1'), (SELECT id FROM sponsorships WHERE title = 'Gaming Sponsorship'), 400.00, 'pending', NOW());
diff --git a/Frontend/.gitignore b/Frontend/.gitignore
deleted file mode 100644
index 6450872..0000000
--- a/Frontend/.gitignore
+++ /dev/null
@@ -1,26 +0,0 @@
-.env*
-
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/Frontend/.npmrc b/Frontend/.npmrc
deleted file mode 100644
index e9ee3cb..0000000
--- a/Frontend/.npmrc
+++ /dev/null
@@ -1 +0,0 @@
-legacy-peer-deps=true
\ No newline at end of file
diff --git a/Frontend/README.md b/Frontend/README.md
deleted file mode 100644
index 40ede56..0000000
--- a/Frontend/README.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# React + TypeScript + Vite
-
-This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
-
-Currently, two official plugins are available:
-
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
-
-## Expanding the ESLint configuration
-
-If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
-
-```js
-export default tseslint.config({
- extends: [
- // Remove ...tseslint.configs.recommended and replace with this
- ...tseslint.configs.recommendedTypeChecked,
- // Alternatively, use this for stricter rules
- ...tseslint.configs.strictTypeChecked,
- // Optionally, add this for stylistic rules
- ...tseslint.configs.stylisticTypeChecked,
- ],
- languageOptions: {
- // other options...
- parserOptions: {
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
- tsconfigRootDir: import.meta.dirname,
- },
- },
-})
-```
-
-You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
-
-```js
-// eslint.config.js
-import reactX from 'eslint-plugin-react-x'
-import reactDom from 'eslint-plugin-react-dom'
-
-export default tseslint.config({
- plugins: {
- // Add the react-x and react-dom plugins
- 'react-x': reactX,
- 'react-dom': reactDom,
- },
- rules: {
- // other rules...
- // Enable its recommended typescript rules
- ...reactX.configs['recommended-typescript'].rules,
- ...reactDom.configs.recommended.rules,
- },
-})
-```
diff --git a/Frontend/components.json b/Frontend/components.json
deleted file mode 100644
index 73afbdb..0000000
--- a/Frontend/components.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "new-york",
- "rsc": false,
- "tsx": true,
- "tailwind": {
- "config": "",
- "css": "src/index.css",
- "baseColor": "neutral",
- "cssVariables": true,
- "prefix": ""
- },
- "aliases": {
- "components": "@/components",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib",
- "hooks": "@/hooks"
- },
- "iconLibrary": "lucide"
-}
\ No newline at end of file
diff --git a/Frontend/env-example b/Frontend/env-example
deleted file mode 100644
index 4ce57da..0000000
--- a/Frontend/env-example
+++ /dev/null
@@ -1,3 +0,0 @@
-VITE_SUPABASE_URL=https://your-project.supabase.co
-VITE_SUPABASE_ANON_KEY=your-anon-key-here
-VITE_YOUTUBE_API_KEY=your-youtube-api-key-here
\ No newline at end of file
diff --git a/Frontend/eslint.config.js b/Frontend/eslint.config.js
deleted file mode 100644
index 0bbf074..0000000
--- a/Frontend/eslint.config.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import js from "@eslint/js";
-import globals from "globals";
-import reactHooks from "eslint-plugin-react-hooks";
-import reactRefresh from "eslint-plugin-react-refresh";
-import tseslint from "typescript-eslint";
-
-export default tseslint.config(
- { ignores: ["dist"] },
- {
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
- files: ["**/*.{ts,tsx}"],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- },
- plugins: {
- "react-hooks": reactHooks,
- "react-refresh": reactRefresh,
- },
- rules: {
- ...reactHooks.configs.recommended.rules,
- "react-refresh/only-export-components": [
- "warn",
- { allowConstantExport: true },
- ],
- },
- }
-);
diff --git a/Frontend/index.html b/Frontend/index.html
deleted file mode 100644
index 304e2d9..0000000
--- a/Frontend/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- Inpact - AI-Powered Creator Collaboration Platform
-
-
-
-
-
-
diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json
deleted file mode 100644
index deae757..0000000
--- a/Frontend/package-lock.json
+++ /dev/null
@@ -1,6241 +0,0 @@
-{
- "name": "frontend",
- "version": "0.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "frontend",
- "version": "0.0.0",
- "dependencies": {
- "@radix-ui/react-avatar": "^1.1.3",
- "@radix-ui/react-dialog": "^1.1.6",
- "@radix-ui/react-dropdown-menu": "^2.1.6",
- "@radix-ui/react-label": "^2.1.2",
- "@radix-ui/react-popover": "^1.1.6",
- "@radix-ui/react-scroll-area": "^1.2.3",
- "@radix-ui/react-select": "^2.1.6",
- "@radix-ui/react-separator": "^1.1.2",
- "@radix-ui/react-slider": "^1.2.3",
- "@radix-ui/react-slot": "^1.1.2",
- "@radix-ui/react-switch": "^1.1.3",
- "@radix-ui/react-tabs": "^1.1.3",
- "@reduxjs/toolkit": "^2.6.1",
- "@supabase/supabase-js": "^2.49.4",
- "@tailwindcss/vite": "^4.0.16",
- "axios": "^1.8.4",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "date-fns": "^4.1.0",
- "dotenv": "^16.4.7",
- "framer-motion": "^12.5.0",
- "lucide-react": "^0.477.0",
- "react": "^19.0.0",
- "react-day-picker": "^8.10.1",
- "react-dom": "^19.0.0",
- "react-redux": "^9.2.0",
- "react-router-dom": "^7.2.0",
- "recharts": "^2.15.1",
- "tailwind-merge": "^3.0.2",
- "tailwindcss": "^4.0.16",
- "tw-animate-css": "^1.2.4"
- },
- "devDependencies": {
- "@eslint/js": "^9.21.0",
- "@types/node": "^22.13.13",
- "@types/react": "^19.0.10",
- "@types/react-dom": "^19.0.4",
- "@vitejs/plugin-react": "^4.3.4",
- "eslint": "^9.21.0",
- "eslint-plugin-react-hooks": "^5.1.0",
- "eslint-plugin-react-refresh": "^0.4.19",
- "globals": "^15.15.0",
- "typescript": "~5.7.2",
- "typescript-eslint": "^8.24.1",
- "vite": "^6.2.0"
- }
- },
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
- "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
- "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.26.10",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
- "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.26.10",
- "@babel/helper-compilation-targets": "^7.26.5",
- "@babel/helper-module-transforms": "^7.26.0",
- "@babel/helpers": "^7.26.10",
- "@babel/parser": "^7.26.10",
- "@babel/template": "^7.26.9",
- "@babel/traverse": "^7.26.10",
- "@babel/types": "^7.26.10",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
- "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.27.0",
- "@babel/types": "^7.27.0",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
- "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.26.8",
- "@babel/helper-validator-option": "^7.25.9",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
- "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
- "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9",
- "@babel/traverse": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
- "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
- "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
- "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
- "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
- "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.27.0",
- "@babel/types": "^7.27.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
- "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.27.0"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
- "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
- "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/runtime": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
- "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
- "license": "MIT",
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
- "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/parser": "^7.27.0",
- "@babel/types": "^7.27.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
- "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.27.0",
- "@babel/parser": "^7.27.0",
- "@babel/template": "^7.27.0",
- "@babel/types": "^7.27.0",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
- "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
- "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
- "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
- "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
- "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
- "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
- "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
- "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
- "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
- "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
- "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
- "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
- "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
- "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
- "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
- "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
- "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
- "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
- "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
- "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
- "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
- "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
- "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
- "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
- "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
- "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@eslint-community/eslint-utils": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz",
- "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eslint-visitor-keys": "^3.4.3"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- },
- "peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
- }
- },
- "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint-community/regexpp": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
- }
- },
- "node_modules/@eslint/config-array": {
- "version": "0.19.2",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
- "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/object-schema": "^2.1.6",
- "debug": "^4.3.1",
- "minimatch": "^3.1.2"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/config-helpers": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz",
- "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/core": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
- "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/eslintrc": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
- "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "^6.12.4",
- "debug": "^4.3.2",
- "espree": "^10.0.1",
- "globals": "^14.0.0",
- "ignore": "^5.2.0",
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
- "minimatch": "^3.1.2",
- "strip-json-comments": "^3.1.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint/eslintrc/node_modules/globals": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@eslint/js": {
- "version": "9.23.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz",
- "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/object-schema": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
- "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/plugin-kit": {
- "version": "0.2.7",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz",
- "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.12.0",
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@floating-ui/core": {
- "version": "1.6.9",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
- "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/utils": "^0.2.9"
- }
- },
- "node_modules/@floating-ui/dom": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
- "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/core": "^1.6.0",
- "@floating-ui/utils": "^0.2.9"
- }
- },
- "node_modules/@floating-ui/react-dom": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
- "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/dom": "^1.0.0"
- },
- "peerDependencies": {
- "react": ">=16.8.0",
- "react-dom": ">=16.8.0"
- }
- },
- "node_modules/@floating-ui/utils": {
- "version": "0.2.9",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
- "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
- "license": "MIT"
- },
- "node_modules/@humanfs/core": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
- "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node": {
- "version": "0.16.6",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
- "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@humanfs/core": "^0.19.1",
- "@humanwhocodes/retry": "^0.3.0"
- },
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
- "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/module-importer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
- "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.22"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/retry": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
- "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
- "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@radix-ui/number": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
- "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==",
- "license": "MIT"
- },
- "node_modules/@radix-ui/primitive": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
- "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
- "license": "MIT"
- },
- "node_modules/@radix-ui/react-arrow": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
- "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.0.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-avatar": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.3.tgz",
- "integrity": "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-collection": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
- "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-slot": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
- "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-context": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
- "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.13.tgz",
- "integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.2",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-context": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.9",
- "@radix-ui/react-focus-guards": "1.1.2",
- "@radix-ui/react-focus-scope": "1.1.6",
- "@radix-ui/react-id": "1.1.1",
- "@radix-ui/react-portal": "1.1.8",
- "@radix-ui/react-presence": "1.1.4",
- "@radix-ui/react-primitive": "2.1.2",
- "@radix-ui/react-slot": "1.2.2",
- "@radix-ui/react-use-controllable-state": "1.2.2",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
- "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
- "license": "MIT"
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
- "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
- "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz",
- "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.2",
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-primitive": "2.1.2",
- "@radix-ui/react-use-callback-ref": "1.1.1",
- "@radix-ui/react-use-escape-keydown": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
- "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz",
- "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-primitive": "2.1.2",
- "@radix-ui/react-use-callback-ref": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
- "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz",
- "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
- "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
- "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.2.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
- "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
- "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
- "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-effect-event": "0.0.2",
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
- "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
- "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-direction": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
- "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
- "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-escape-keydown": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-dropdown-menu": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz",
- "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-menu": "2.1.6",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-controllable-state": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-focus-guards": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
- "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-focus-scope": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
- "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-callback-ref": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-id": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
- "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-label": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz",
- "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.0.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-menu": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz",
- "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collection": "1.1.2",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-dismissable-layer": "1.1.5",
- "@radix-ui/react-focus-guards": "1.1.1",
- "@radix-ui/react-focus-scope": "1.1.2",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-popper": "1.2.2",
- "@radix-ui/react-portal": "1.1.4",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-roving-focus": "1.1.2",
- "@radix-ui/react-slot": "1.1.2",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-popover": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz",
- "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.5",
- "@radix-ui/react-focus-guards": "1.1.1",
- "@radix-ui/react-focus-scope": "1.1.2",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-popper": "1.2.2",
- "@radix-ui/react-portal": "1.1.4",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-slot": "1.1.2",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-popper": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
- "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/react-dom": "^2.0.0",
- "@radix-ui/react-arrow": "1.1.2",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0",
- "@radix-ui/react-use-rect": "1.1.0",
- "@radix-ui/react-use-size": "1.1.0",
- "@radix-ui/rect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-portal": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
- "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-layout-effect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-presence": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
- "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-use-layout-effect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-primitive": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
- "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-roving-focus": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
- "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collection": "1.1.2",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-controllable-state": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-scroll-area": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz",
- "integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/number": "1.1.0",
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-select": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",
- "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/number": "1.1.0",
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collection": "1.1.2",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-dismissable-layer": "1.1.5",
- "@radix-ui/react-focus-guards": "1.1.1",
- "@radix-ui/react-focus-scope": "1.1.2",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-popper": "1.2.2",
- "@radix-ui/react-portal": "1.1.4",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-slot": "1.1.2",
- "@radix-ui/react-use-callback-ref": "1.1.0",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0",
- "@radix-ui/react-use-previous": "1.1.0",
- "@radix-ui/react-visually-hidden": "1.1.2",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.6.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-separator": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
- "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.0.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-slider": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.3.tgz",
- "integrity": "sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/number": "1.1.0",
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-collection": "1.1.2",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0",
- "@radix-ui/react-use-previous": "1.1.0",
- "@radix-ui/react-use-size": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-slot": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
- "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-switch": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz",
- "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "@radix-ui/react-use-previous": "1.1.0",
- "@radix-ui/react-use-size": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-tabs": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
- "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-direction": "1.1.0",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.2",
- "@radix-ui/react-roving-focus": "1.1.2",
- "@radix-ui/react-use-controllable-state": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-callback-ref": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
- "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-controllable-state": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
- "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-effect-event": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
- "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.1"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
- "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-escape-keydown": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
- "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
- "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-previous": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
- "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-rect": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
- "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/rect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-use-size": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
- "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/react-visually-hidden": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
- "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-primitive": "2.0.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@radix-ui/rect": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
- "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
- "license": "MIT"
- },
- "node_modules/@reduxjs/toolkit": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.0.tgz",
- "integrity": "sha512-7OAPcjqZwxzTV9UQ5l6hKQ9ap9GV1xJi6mh6hzDm+qvEjZ4hRdWMBx9b5oE8k1X9PQY8aE/Zf0WBKAYw0digXg==",
- "license": "MIT",
- "dependencies": {
- "@standard-schema/spec": "^1.0.0",
- "@standard-schema/utils": "^0.3.0",
- "immer": "^10.0.3",
- "redux": "^5.0.1",
- "redux-thunk": "^3.1.0",
- "reselect": "^5.1.0"
- },
- "peerDependencies": {
- "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
- "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
- },
- "peerDependenciesMeta": {
- "react": {
- "optional": true
- },
- "react-redux": {
- "optional": true
- }
- }
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz",
- "integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz",
- "integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz",
- "integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz",
- "integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz",
- "integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz",
- "integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz",
- "integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz",
- "integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz",
- "integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz",
- "integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz",
- "integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz",
- "integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz",
- "integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz",
- "integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz",
- "integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz",
- "integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz",
- "integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz",
- "integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz",
- "integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz",
- "integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@standard-schema/spec": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
- "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
- "license": "MIT"
- },
- "node_modules/@standard-schema/utils": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
- "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
- "license": "MIT"
- },
- "node_modules/@supabase/auth-js": {
- "version": "2.69.1",
- "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz",
- "integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "^2.6.14"
- }
- },
- "node_modules/@supabase/functions-js": {
- "version": "2.4.4",
- "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz",
- "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "^2.6.14"
- }
- },
- "node_modules/@supabase/node-fetch": {
- "version": "2.6.15",
- "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
- "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
- "license": "MIT",
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- }
- },
- "node_modules/@supabase/postgrest-js": {
- "version": "1.19.4",
- "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz",
- "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "^2.6.14"
- }
- },
- "node_modules/@supabase/realtime-js": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz",
- "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "^2.6.14",
- "@types/phoenix": "^1.5.4",
- "@types/ws": "^8.5.10",
- "ws": "^8.18.0"
- }
- },
- "node_modules/@supabase/storage-js": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
- "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "^2.6.14"
- }
- },
- "node_modules/@supabase/supabase-js": {
- "version": "2.49.4",
- "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz",
- "integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==",
- "license": "MIT",
- "dependencies": {
- "@supabase/auth-js": "2.69.1",
- "@supabase/functions-js": "2.4.4",
- "@supabase/node-fetch": "2.6.15",
- "@supabase/postgrest-js": "1.19.4",
- "@supabase/realtime-js": "2.11.2",
- "@supabase/storage-js": "2.7.1"
- }
- },
- "node_modules/@tailwindcss/node": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.16.tgz",
- "integrity": "sha512-T6IK79hoCFScxD5tRxWMtwqwSs4sT81Vw+YbzL7RZD0/Ndm4y5kboV7LdQ97YGH6udoOZyVT/uEfrnU2L5Nkog==",
- "license": "MIT",
- "dependencies": {
- "enhanced-resolve": "^5.18.1",
- "jiti": "^2.4.2",
- "tailwindcss": "4.0.16"
- }
- },
- "node_modules/@tailwindcss/oxide": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.16.tgz",
- "integrity": "sha512-n++F8Rzvo/e+FYxikZgKW4sCRXneSstLhTI91Ay9toeRcE/+WO33SQWzGtgmjWJcTupXZreskJ8FCr9b+kdXew==",
- "license": "MIT",
- "engines": {
- "node": ">= 10"
- },
- "optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.0.16",
- "@tailwindcss/oxide-darwin-arm64": "4.0.16",
- "@tailwindcss/oxide-darwin-x64": "4.0.16",
- "@tailwindcss/oxide-freebsd-x64": "4.0.16",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.16",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.0.16",
- "@tailwindcss/oxide-linux-arm64-musl": "4.0.16",
- "@tailwindcss/oxide-linux-x64-gnu": "4.0.16",
- "@tailwindcss/oxide-linux-x64-musl": "4.0.16",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.0.16",
- "@tailwindcss/oxide-win32-x64-msvc": "4.0.16"
- }
- },
- "node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.16.tgz",
- "integrity": "sha512-mieEZrNLHatpQu6ad0pWBnL8ObUE9ZSe4eoX6GKTqsKv98AxNw5lUa5nJM0FgD8rYJeZ2dPtHNN/YM2xY9R+9g==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.16.tgz",
- "integrity": "sha512-pfilSvgrX5UDdjh09gGVMhAPfZVucm4AnwFBkwBe6WFl7gzMAZ92/35GC0yMDeS+W+RNSXclXJz+HamF1iS/aA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.16.tgz",
- "integrity": "sha512-Z3lJY3yUjlHbzgXwWH9Y6IGeSGXfwjbXuvTPolyJUGMZl2ZaHdQMPOZ8dMll1knSLjctOif+QijMab0+GSXYLQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.16.tgz",
- "integrity": "sha512-dv2U8Yc7vKIDyiJkUouhjsl+dTfRImNyZRCTFsHvvrhJvenYZBRtE/wDSYlZHR0lWKhIocxk1ScAkAcMR3F3QQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.16.tgz",
- "integrity": "sha512-XBRXyUUyjMg5UMiyuQxJqWSs27w0V49g1iPuhrFakmu1/idDSly59XYteRrI2onoS9AzmMwfyzdiQSJXM89+PQ==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.16.tgz",
- "integrity": "sha512-+bL1zkU8MDzv389OqyI0SJbrG9kGsdxf+k2ZAILlw1TPWg5oeMkwoqgaQRqGwpOHz0pycT94qIgWVNJavAz+Iw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.16.tgz",
- "integrity": "sha512-Uqfnyx9oFxoX+/iy9pIDTADHLLNwuZNB8QSp+BwKAhtHjBTTYmDAdxKy3u8lJZve1aOd+S145eWpn3tT08cm4w==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.16.tgz",
- "integrity": "sha512-v0Hx0KD94F6FG0IW3AJyCzQepSv/47xhShCgiWJ2TNVu406VtREkGpJtxS0Gu1ecSXhgn/36LToU5kivAuQiPg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.16.tgz",
- "integrity": "sha512-CjV6hhQAVNYw6W2EXp1ZVL81CTSBEh6nTmS5EZq5rdEhqOx8G8YQtFKjcCJiojsS+vMXt9r87gGoORJcHOA0lg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.16.tgz",
- "integrity": "sha512-Pj9eaAtXYH7NrvVx8Jx0U/sEaNpcIbb8d+2WnC8a+xL0LfIXWsu4AyeRUeTeb8Ty4fTGhKSJTohdXj1iSdN9WQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.16.tgz",
- "integrity": "sha512-M35hoFrhJe+1QdSiZpn85y8K7tfEVw6lswv3TjIfJ44JiPjPzZ4URg+rsTjTq0kue6NjNCbbY99AsRSSpJZxOw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@tailwindcss/vite": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.16.tgz",
- "integrity": "sha512-6mZVWhAyjVNMMRw0Pvv2RZfTttjsAClU8HouLNZbeLbX0yURMa0UYEY/qS4dB1tZlRpiDBnCLsGsWbxEyIjW6A==",
- "license": "MIT",
- "dependencies": {
- "@tailwindcss/node": "4.0.16",
- "@tailwindcss/oxide": "4.0.16",
- "lightningcss": "1.29.2",
- "tailwindcss": "4.0.16"
- },
- "peerDependencies": {
- "vite": "^5.2.0 || ^6"
- }
- },
- "node_modules/@types/babel__core": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7",
- "@types/babel__generator": "*",
- "@types/babel__template": "*",
- "@types/babel__traverse": "*"
- }
- },
- "node_modules/@types/babel__generator": {
- "version": "7.6.8",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
- "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.1.0",
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__traverse": {
- "version": "7.20.6",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
- "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.20.7"
- }
- },
- "node_modules/@types/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
- "license": "MIT"
- },
- "node_modules/@types/d3-array": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
- "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
- "license": "MIT"
- },
- "node_modules/@types/d3-color": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
- "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
- "license": "MIT"
- },
- "node_modules/@types/d3-ease": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
- "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
- "license": "MIT"
- },
- "node_modules/@types/d3-interpolate": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
- "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-color": "*"
- }
- },
- "node_modules/@types/d3-path": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
- "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
- "license": "MIT"
- },
- "node_modules/@types/d3-scale": {
- "version": "4.0.9",
- "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
- "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-time": "*"
- }
- },
- "node_modules/@types/d3-shape": {
- "version": "3.1.7",
- "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
- "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-path": "*"
- }
- },
- "node_modules/@types/d3-time": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
- "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
- "license": "MIT"
- },
- "node_modules/@types/d3-timer": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
- "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
- "license": "MIT"
- },
- "node_modules/@types/estree": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
- "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "22.13.13",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz",
- "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.20.0"
- }
- },
- "node_modules/@types/phoenix": {
- "version": "1.6.6",
- "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
- "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
- "license": "MIT"
- },
- "node_modules/@types/react": {
- "version": "19.0.12",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
- "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "19.0.4",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz",
- "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "^19.0.0"
- }
- },
- "node_modules/@types/use-sync-external-store": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
- "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
- "license": "MIT"
- },
- "node_modules/@types/ws": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
- "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.28.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz",
- "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.28.0",
- "@typescript-eslint/type-utils": "8.28.0",
- "@typescript-eslint/utils": "8.28.0",
- "@typescript-eslint/visitor-keys": "8.28.0",
- "graphemer": "^1.4.0",
- "ignore": "^5.3.1",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^2.0.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/parser": {
- "version": "8.28.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz",
- "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/scope-manager": "8.28.0",
- "@typescript-eslint/types": "8.28.0",
- "@typescript-eslint/typescript-estree": "8.28.0",
- "@typescript-eslint/visitor-keys": "8.28.0",
- "debug": "^4.3.4"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.28.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz",
- "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.28.0",
- "@typescript-eslint/visitor-keys": "8.28.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/type-utils": {
- "version": "8.28.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz",
- "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/typescript-estree": "8.28.0",
- "@typescript-eslint/utils": "8.28.0",
- "debug": "^4.3.4",
- "ts-api-utils": "^2.0.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/types": {
- "version": "8.28.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz",
- "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.28.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz",
- "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.28.0",
- "@typescript-eslint/visitor-keys": "8.28.0",
- "debug": "^4.3.4",
- "fast-glob": "^3.3.2",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^2.0.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@typescript-eslint/utils": {
- "version": "8.28.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz",
- "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "8.28.0",
- "@typescript-eslint/types": "8.28.0",
- "@typescript-eslint/typescript-estree": "8.28.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.28.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz",
- "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.28.0",
- "eslint-visitor-keys": "^4.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@vitejs/plugin-react": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
- "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.26.0",
- "@babel/plugin-transform-react-jsx-self": "^7.25.9",
- "@babel/plugin-transform-react-jsx-source": "^7.25.9",
- "@types/babel__core": "^7.20.5",
- "react-refresh": "^0.14.2"
- },
- "engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
- }
- },
- "node_modules/acorn": {
- "version": "8.14.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
- "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
- "license": "Python-2.0"
- },
- "node_modules/aria-hidden": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
- "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT"
- },
- "node_modules/axios": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
- "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
- "license": "MIT",
- "dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
- "proxy-from-env": "^1.1.0"
- }
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/browserslist": {
- "version": "4.24.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
- "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "caniuse-lite": "^1.0.30001688",
- "electron-to-chromium": "^1.5.73",
- "node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.1"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001707",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
- "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
- },
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/class-variance-authority": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
- "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
- "license": "Apache-2.0",
- "dependencies": {
- "clsx": "^2.1.1"
- },
- "funding": {
- "url": "https://polar.sh/cva"
- }
- },
- "node_modules/clsx": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "license": "MIT",
- "dependencies": {
- "delayed-stream": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cookie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
- "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "license": "MIT"
- },
- "node_modules/d3-array": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
- "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
- "license": "ISC",
- "dependencies": {
- "internmap": "1 - 2"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-color": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
- "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-ease": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
- "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-format": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
- "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-interpolate": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
- "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
- "license": "ISC",
- "dependencies": {
- "d3-color": "1 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-path": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
- "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-scale": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
- "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
- "license": "ISC",
- "dependencies": {
- "d3-array": "2.10.0 - 3",
- "d3-format": "1 - 3",
- "d3-interpolate": "1.2.0 - 3",
- "d3-time": "2.1.1 - 3",
- "d3-time-format": "2 - 4"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-shape": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
- "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
- "license": "ISC",
- "dependencies": {
- "d3-path": "^3.1.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-time": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
- "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
- "license": "ISC",
- "dependencies": {
- "d3-array": "2 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-time-format": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
- "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
- "license": "ISC",
- "dependencies": {
- "d3-time": "1 - 3"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-timer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
- "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/date-fns": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
- "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/kossnocorp"
- }
- },
- "node_modules/debug": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
- "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/decimal.js-light": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
- "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
- "license": "MIT"
- },
- "node_modules/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
- "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/detect-node-es": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
- "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
- "license": "MIT"
- },
- "node_modules/dom-helpers": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
- "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.8.7",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/dotenv": {
- "version": "16.5.0",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
- "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.123",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.123.tgz",
- "integrity": "sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/enhanced-resolve": {
- "version": "5.18.1",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
- "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-set-tostringtag": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
- "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/esbuild": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
- "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.1",
- "@esbuild/android-arm": "0.25.1",
- "@esbuild/android-arm64": "0.25.1",
- "@esbuild/android-x64": "0.25.1",
- "@esbuild/darwin-arm64": "0.25.1",
- "@esbuild/darwin-x64": "0.25.1",
- "@esbuild/freebsd-arm64": "0.25.1",
- "@esbuild/freebsd-x64": "0.25.1",
- "@esbuild/linux-arm": "0.25.1",
- "@esbuild/linux-arm64": "0.25.1",
- "@esbuild/linux-ia32": "0.25.1",
- "@esbuild/linux-loong64": "0.25.1",
- "@esbuild/linux-mips64el": "0.25.1",
- "@esbuild/linux-ppc64": "0.25.1",
- "@esbuild/linux-riscv64": "0.25.1",
- "@esbuild/linux-s390x": "0.25.1",
- "@esbuild/linux-x64": "0.25.1",
- "@esbuild/netbsd-arm64": "0.25.1",
- "@esbuild/netbsd-x64": "0.25.1",
- "@esbuild/openbsd-arm64": "0.25.1",
- "@esbuild/openbsd-x64": "0.25.1",
- "@esbuild/sunos-x64": "0.25.1",
- "@esbuild/win32-arm64": "0.25.1",
- "@esbuild/win32-ia32": "0.25.1",
- "@esbuild/win32-x64": "0.25.1"
- }
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint": {
- "version": "9.23.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz",
- "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.19.2",
- "@eslint/config-helpers": "^0.2.0",
- "@eslint/core": "^0.12.0",
- "@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.23.0",
- "@eslint/plugin-kit": "^0.2.7",
- "@humanfs/node": "^0.16.6",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.4.2",
- "@types/estree": "^1.0.6",
- "@types/json-schema": "^7.0.15",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.3.0",
- "eslint-visitor-keys": "^4.2.0",
- "espree": "^10.3.0",
- "esquery": "^1.5.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "jiti": "*"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-plugin-react-hooks": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
- "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
- }
- },
- "node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.19",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz",
- "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "eslint": ">=8.40"
- }
- },
- "node_modules/eslint-scope": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
- "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-visitor-keys": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/espree": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
- "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "acorn": "^8.14.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/esquery": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
- "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "estraverse": "^5.1.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/eventemitter3": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
- "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
- "license": "MIT"
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-equals": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
- "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/fast-glob": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
- "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.8"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fastq": {
- "version": "1.19.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
- "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "reusify": "^1.0.4"
- }
- },
- "node_modules/file-entry-cache": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flat-cache": "^4.0.0"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
- },
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/follow-redirects": {
- "version": "1.15.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
- "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
- "node_modules/form-data": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
- "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
- "license": "MIT",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/framer-motion": {
- "version": "12.6.0",
- "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.0.tgz",
- "integrity": "sha512-91XLZ3VwDlXe9u2ABhTzYBiFQ/qdoiqyTiTCQDDJ4es5/5lzp76hdB+WG7gcNklcQlOmfDZQqVO48tqzY9Z/bQ==",
- "license": "MIT",
- "dependencies": {
- "motion-dom": "^12.6.0",
- "motion-utils": "^12.5.0",
- "tslib": "^2.4.0"
- },
- "peerDependencies": {
- "@emotion/is-prop-valid": "*",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/is-prop-valid": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-nonce": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
- "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/globals": {
- "version": "15.15.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
- "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "license": "ISC"
- },
- "node_modules/graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "license": "MIT",
- "dependencies": {
- "has-symbols": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/ignore": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/immer": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
- "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/immer"
- }
- },
- "node_modules/import-fresh": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
- "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/internmap": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
- "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/jiti": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
- "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
- "license": "MIT",
- "bin": {
- "jiti": "lib/jiti-cli.mjs"
- }
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
- },
- "node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "json-buffer": "3.0.1"
- }
- },
- "node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/lightningcss": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
- "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-darwin-arm64": "1.29.2",
- "lightningcss-darwin-x64": "1.29.2",
- "lightningcss-freebsd-x64": "1.29.2",
- "lightningcss-linux-arm-gnueabihf": "1.29.2",
- "lightningcss-linux-arm64-gnu": "1.29.2",
- "lightningcss-linux-arm64-musl": "1.29.2",
- "lightningcss-linux-x64-gnu": "1.29.2",
- "lightningcss-linux-x64-musl": "1.29.2",
- "lightningcss-win32-arm64-msvc": "1.29.2",
- "lightningcss-win32-x64-msvc": "1.29.2"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
- "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
- "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
- "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
- "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
- "cpu": [
- "arm"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
- "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
- "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
- "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
- "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
- "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
- "cpu": [
- "arm64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.29.2",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
- "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
- "cpu": [
- "x64"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "license": "MIT"
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/lucide-react": {
- "version": "0.477.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.477.0.tgz",
- "integrity": "sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw==",
- "license": "ISC",
- "peerDependencies": {
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/micromatch": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/motion-dom": {
- "version": "12.6.0",
- "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.0.tgz",
- "integrity": "sha512-1s/+/V0ny/gfhocSSf0qhkspZK2da7jrwGw7xHzgiQPcimdHaPRcRCoJ3OxEZYBNzy3ma1ERUD+eUStk6a9pQw==",
- "license": "MIT",
- "dependencies": {
- "motion-utils": "^12.5.0"
- }
- },
- "node_modules/motion-utils": {
- "version": "12.5.0",
- "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.5.0.tgz",
- "integrity": "sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA==",
- "license": "MIT"
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/optionator": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
- "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.5"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.3",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
- "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.8",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
- "node_modules/prop-types/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
- "node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/react": {
- "version": "19.0.0",
- "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
- "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-day-picker": {
- "version": "8.10.1",
- "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
- "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==",
- "license": "MIT",
- "funding": {
- "type": "individual",
- "url": "https://github.com/sponsors/gpbl"
- },
- "peerDependencies": {
- "date-fns": "^2.28.0 || ^3.0.0",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/react-dom": {
- "version": "19.0.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
- "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
- "license": "MIT",
- "dependencies": {
- "scheduler": "^0.25.0"
- },
- "peerDependencies": {
- "react": "^19.0.0"
- }
- },
- "node_modules/react-is": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
- "license": "MIT"
- },
- "node_modules/react-redux": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
- "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
- "license": "MIT",
- "dependencies": {
- "@types/use-sync-external-store": "^0.0.6",
- "use-sync-external-store": "^1.4.0"
- },
- "peerDependencies": {
- "@types/react": "^18.2.25 || ^19",
- "react": "^18.0 || ^19",
- "redux": "^5.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "redux": {
- "optional": true
- }
- }
- },
- "node_modules/react-refresh": {
- "version": "0.14.2",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
- "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-remove-scroll": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
- "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
- "license": "MIT",
- "dependencies": {
- "react-remove-scroll-bar": "^2.3.7",
- "react-style-singleton": "^2.2.3",
- "tslib": "^2.1.0",
- "use-callback-ref": "^1.3.3",
- "use-sidecar": "^1.1.3"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-remove-scroll-bar": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
- "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
- "license": "MIT",
- "dependencies": {
- "react-style-singleton": "^2.2.2",
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-router": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.0.tgz",
- "integrity": "sha512-Y2g5ObjkvX3VFeVt+0CIPuYd9PpgqCslG7ASSIdN73LwA1nNWzcMLaoMRJfP3prZFI92svxFwbn7XkLJ+UPQ6A==",
- "license": "MIT",
- "dependencies": {
- "@types/cookie": "^0.6.0",
- "cookie": "^1.0.1",
- "set-cookie-parser": "^2.6.0",
- "turbo-stream": "2.4.0"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/react-router-dom": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.0.tgz",
- "integrity": "sha512-VlksBPf3n2bijPvnA7nkTsXxMAKOj+bWp4R9c3i+bnwlSOFAGOkJkKhzy/OsRkWaBMICqcAl1JDzh9ZSOze9CA==",
- "license": "MIT",
- "dependencies": {
- "react-router": "7.4.0"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- }
- },
- "node_modules/react-smooth": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
- "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
- "license": "MIT",
- "dependencies": {
- "fast-equals": "^5.0.1",
- "prop-types": "^15.8.1",
- "react-transition-group": "^4.4.5"
- },
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/react-style-singleton": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
- "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
- "license": "MIT",
- "dependencies": {
- "get-nonce": "^1.0.0",
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-transition-group": {
- "version": "4.4.5",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
- "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/runtime": "^7.5.5",
- "dom-helpers": "^5.0.1",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.6.2"
- },
- "peerDependencies": {
- "react": ">=16.6.0",
- "react-dom": ">=16.6.0"
- }
- },
- "node_modules/recharts": {
- "version": "2.15.1",
- "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz",
- "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==",
- "license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0",
- "eventemitter3": "^4.0.1",
- "lodash": "^4.17.21",
- "react-is": "^18.3.1",
- "react-smooth": "^4.0.4",
- "recharts-scale": "^0.4.4",
- "tiny-invariant": "^1.3.1",
- "victory-vendor": "^36.6.8"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/recharts-scale": {
- "version": "0.4.5",
- "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
- "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
- "license": "MIT",
- "dependencies": {
- "decimal.js-light": "^2.4.1"
- }
- },
- "node_modules/redux": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
- "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "license": "MIT"
- },
- "node_modules/redux-thunk": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
- "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
- "license": "MIT",
- "peerDependencies": {
- "redux": "^5.0.0"
- }
- },
- "node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "license": "MIT"
- },
- "node_modules/reselect": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
- "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
- "license": "MIT"
- },
- "node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/reusify": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
- "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
- "node_modules/rollup": {
- "version": "4.37.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz",
- "integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.6"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.37.0",
- "@rollup/rollup-android-arm64": "4.37.0",
- "@rollup/rollup-darwin-arm64": "4.37.0",
- "@rollup/rollup-darwin-x64": "4.37.0",
- "@rollup/rollup-freebsd-arm64": "4.37.0",
- "@rollup/rollup-freebsd-x64": "4.37.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.37.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.37.0",
- "@rollup/rollup-linux-arm64-gnu": "4.37.0",
- "@rollup/rollup-linux-arm64-musl": "4.37.0",
- "@rollup/rollup-linux-loongarch64-gnu": "4.37.0",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.37.0",
- "@rollup/rollup-linux-riscv64-musl": "4.37.0",
- "@rollup/rollup-linux-s390x-gnu": "4.37.0",
- "@rollup/rollup-linux-x64-gnu": "4.37.0",
- "@rollup/rollup-linux-x64-musl": "4.37.0",
- "@rollup/rollup-win32-arm64-msvc": "4.37.0",
- "@rollup/rollup-win32-ia32-msvc": "4.37.0",
- "@rollup/rollup-win32-x64-msvc": "4.37.0",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/rollup/node_modules/@types/estree": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
- "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "queue-microtask": "^1.2.2"
- }
- },
- "node_modules/scheduler": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
- "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
- "license": "MIT"
- },
- "node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/set-cookie-parser": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
- "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
- "license": "MIT"
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/tailwind-merge": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
- "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/dcastil"
- }
- },
- "node_modules/tailwindcss": {
- "version": "4.0.16",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.16.tgz",
- "integrity": "sha512-i/SbG7ThTIcLshcFJL+je7hCv9dPis4Xl4XNeel6iZNX42pp/BZ+la+SbZIPoYE+PN8zhKbnHblpQ/lhOWwIeQ==",
- "license": "MIT"
- },
- "node_modules/tapable": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
- "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/tiny-invariant": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
- "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
- "license": "MIT"
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "license": "MIT"
- },
- "node_modules/ts-api-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
- "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18.12"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
- "node_modules/turbo-stream": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
- "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
- "license": "ISC"
- },
- "node_modules/tw-animate-css": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.4.tgz",
- "integrity": "sha512-yt+HkJB41NAvOffe4NweJU6fLqAlVx/mBX6XmHRp15kq0JxTtOKaIw8pVSWM1Z+n2nXtyi7cW6C9f0WG/F/QAQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Wombosvideo"
- }
- },
- "node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/typescript": {
- "version": "5.7.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
- "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/typescript-eslint": {
- "version": "8.28.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.28.0.tgz",
- "integrity": "sha512-jfZtxJoHm59bvoCMYCe2BM0/baMswRhMmYhy+w6VfcyHrjxZ0OJe0tGasydCpIpA+A/WIJhTyZfb3EtwNC/kHQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/eslint-plugin": "8.28.0",
- "@typescript-eslint/parser": "8.28.0",
- "@typescript-eslint/utils": "8.28.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/undici-types": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
- "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
- "license": "MIT"
- },
- "node_modules/update-browserslist-db": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
- "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
- "node_modules/use-callback-ref": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
- "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/use-sidecar": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
- "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
- "license": "MIT",
- "dependencies": {
- "detect-node-es": "^1.1.0",
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/use-sync-external-store": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
- "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/victory-vendor": {
- "version": "36.9.2",
- "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
- "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
- "license": "MIT AND ISC",
- "dependencies": {
- "@types/d3-array": "^3.0.3",
- "@types/d3-ease": "^3.0.0",
- "@types/d3-interpolate": "^3.0.1",
- "@types/d3-scale": "^4.0.2",
- "@types/d3-shape": "^3.1.0",
- "@types/d3-time": "^3.0.0",
- "@types/d3-timer": "^3.0.0",
- "d3-array": "^3.1.6",
- "d3-ease": "^3.0.1",
- "d3-interpolate": "^3.0.1",
- "d3-scale": "^4.0.2",
- "d3-shape": "^3.1.0",
- "d3-time": "^3.0.0",
- "d3-timer": "^3.0.1"
- }
- },
- "node_modules/vite": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
- "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "^0.25.0",
- "postcss": "^8.5.3",
- "rollup": "^4.30.1"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "jiti": ">=1.21.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "license": "BSD-2-Clause"
- },
- "node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "license": "MIT",
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/word-wrap": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
- "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/ws": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
- "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/yocto-queue": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- }
- }
-}
diff --git a/Frontend/package.json b/Frontend/package.json
deleted file mode 100644
index 1f4ad6f..0000000
--- a/Frontend/package.json
+++ /dev/null
@@ -1,59 +0,0 @@
-{
- "name": "frontend",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc -b && vite build",
- "lint": "eslint .",
- "preview": "vite preview"
- },
- "dependencies": {
- "@radix-ui/react-avatar": "^1.1.3",
- "@radix-ui/react-dialog": "^1.1.6",
- "@radix-ui/react-dropdown-menu": "^2.1.6",
- "@radix-ui/react-label": "^2.1.2",
- "@radix-ui/react-popover": "^1.1.6",
- "@radix-ui/react-scroll-area": "^1.2.3",
- "@radix-ui/react-select": "^2.1.6",
- "@radix-ui/react-separator": "^1.1.2",
- "@radix-ui/react-slider": "^1.2.3",
- "@radix-ui/react-slot": "^1.1.2",
- "@radix-ui/react-switch": "^1.1.3",
- "@radix-ui/react-tabs": "^1.1.3",
- "@reduxjs/toolkit": "^2.6.1",
- "@supabase/supabase-js": "^2.49.4",
- "@tailwindcss/vite": "^4.0.16",
- "axios": "^1.8.4",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "date-fns": "^4.1.0",
- "dotenv": "^16.4.7",
- "framer-motion": "^12.5.0",
- "lucide-react": "^0.477.0",
- "react": "^19.0.0",
- "react-day-picker": "^8.10.1",
- "react-dom": "^19.0.0",
- "react-redux": "^9.2.0",
- "react-router-dom": "^7.2.0",
- "recharts": "^2.15.1",
- "tailwind-merge": "^3.0.2",
- "tailwindcss": "^4.0.16",
- "tw-animate-css": "^1.2.4"
- },
- "devDependencies": {
- "@eslint/js": "^9.21.0",
- "@types/node": "^22.13.13",
- "@types/react": "^19.0.10",
- "@types/react-dom": "^19.0.4",
- "@vitejs/plugin-react": "^4.3.4",
- "eslint": "^9.21.0",
- "eslint-plugin-react-hooks": "^5.1.0",
- "eslint-plugin-react-refresh": "^0.4.19",
- "globals": "^15.15.0",
- "typescript": "~5.7.2",
- "typescript-eslint": "^8.24.1",
- "vite": "^6.2.0"
- }
-}
diff --git a/Frontend/public/Home.png b/Frontend/public/Home.png
deleted file mode 100644
index 8729305..0000000
Binary files a/Frontend/public/Home.png and /dev/null differ
diff --git a/Frontend/public/brand.png b/Frontend/public/brand.png
deleted file mode 100644
index bc29669..0000000
Binary files a/Frontend/public/brand.png and /dev/null differ
diff --git a/Frontend/public/contnetcreator.png b/Frontend/public/contnetcreator.png
deleted file mode 100644
index e527063..0000000
Binary files a/Frontend/public/contnetcreator.png and /dev/null differ
diff --git a/Frontend/public/facebook.png b/Frontend/public/facebook.png
deleted file mode 100644
index 0c37594..0000000
Binary files a/Frontend/public/facebook.png and /dev/null differ
diff --git a/Frontend/public/instagram.png b/Frontend/public/instagram.png
deleted file mode 100644
index 82216ba..0000000
Binary files a/Frontend/public/instagram.png and /dev/null differ
diff --git a/Frontend/public/tiktok.png b/Frontend/public/tiktok.png
deleted file mode 100644
index 4b6a8ae..0000000
Binary files a/Frontend/public/tiktok.png and /dev/null differ
diff --git a/Frontend/public/youtube.png b/Frontend/public/youtube.png
deleted file mode 100644
index 2db89d2..0000000
Binary files a/Frontend/public/youtube.png and /dev/null differ
diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx
deleted file mode 100644
index 60f7ecd..0000000
--- a/Frontend/src/App.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
-import { useState, useEffect } from "react";
-import HomePage from "../src/pages/HomePage";
-import DashboardPage from "../src/pages/DashboardPage";
-import SponsorshipsPage from "../src/pages/Sponsorships";
-import CollaborationsPage from "../src/pages/Collaborations";
-import CollaborationDetails from "../src/pages/CollaborationDetails";
-import MessagesPage from "../src/pages/Messages";
-import LoginPage from "./pages/Login";
-import SignupPage from "./pages/Signup";
-import ForgotPasswordPage from "./pages/ForgotPassword";
-import ResetPasswordPage from "./pages/ResetPassword";
-import Contracts from "./pages/Contracts";
-import Analytics from "./pages/Analytics";
-import RoleSelection from "./pages/RoleSelection";
-
-import { AuthProvider } from "./context/AuthContext";
-import ProtectedRoute from "./components/ProtectedRoute";
-import PublicRoute from "./components/PublicRoute";
-import Dashboard from "./pages/Brand/Dashboard";
-import BasicDetails from "./pages/BasicDetails";
-import Onboarding from "./components/Onboarding";
-
-function App() {
- const [isLoading, setIsLoading] = useState(true);
-
- useEffect(() => {
- // Set a timeout to ensure the app loads
- const timer = setTimeout(() => {
- setIsLoading(false);
- }, 2000);
-
- return () => clearTimeout(timer);
- }, []);
-
- if (isLoading) {
- return (
-
-
Loading Inpact...
-
Connecting to the platform
-
- );
- }
-
- return (
-
-
-
- {/* Public Routes */}
- } />
-
-
-
- } />
-
-
-
- } />
- } />
- Brand Onboarding (Coming Soon)} />
- Creator Onboarding (Coming Soon)} />
- } />
- } />
-
-
-
- } />
- } />
- } />
-
-
-
- } />
-
- {/* Protected Routes*/}
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- );
-}
-
-export default App;
diff --git a/Frontend/src/assets/react.svg b/Frontend/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/Frontend/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/Frontend/src/components/Onboarding.tsx b/Frontend/src/components/Onboarding.tsx
deleted file mode 100644
index 950b09e..0000000
--- a/Frontend/src/components/Onboarding.tsx
+++ /dev/null
@@ -1,1496 +0,0 @@
-import { useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-import { Info } from "lucide-react";
-import { supabase } from "../utils/supabase";
-
-const platforms = [
- { name: "YouTube", icon: "/youtube.png" },
- { name: "Instagram", icon: "/instagram.png" },
- { name: "Facebook", icon: "/facebook.png" },
- { name: "TikTok", icon: "/tiktok.png" },
-];
-
-const steps = [
- "Role Selection",
- "Personal Details",
- "Platform Selection",
- "Platform Details",
- "Pricing",
- "Profile Picture",
- "Review & Submit",
-];
-
-// const YOUTUBE_API_KEY = import.meta.env.VITE_YOUTUBE_API_KEY; // No longer needed in frontend
-
-type BrandData = {
- brand_name: string;
- logo: File | null;
- website_url: string;
- industry: string;
- company_size: string;
- location: string;
- description: string;
- contact_person: string;
- contact_email: string;
- contact_phone: string;
- role: string;
- platforms: string[];
- social_links: Record;
- collaboration_types: string[];
- preferred_creator_categories: string[];
- brand_values: string[];
- preferred_tone: string[];
-};
-
-const brandInitialState: BrandData = {
- brand_name: "",
- logo: null,
- website_url: "",
- industry: "",
- company_size: "",
- location: "",
- description: "",
- contact_person: "",
- contact_email: "",
- contact_phone: "",
- role: "",
- platforms: [],
- social_links: {},
- collaboration_types: [],
- preferred_creator_categories: [],
- brand_values: [],
- preferred_tone: [],
-};
-
-export default function Onboarding() {
- const navigate = useNavigate();
- const { user } = useAuth();
- const [step, setStep] = useState(0);
- const [role, setRole] = useState("");
- const [personal, setPersonal] = useState({ name: "", email: "", age: "", gender: "", country: "", category: "", otherCategory: "" });
- const [selectedPlatforms, setSelectedPlatforms] = useState([]);
- const [platformDetails, setPlatformDetails] = useState({});
- const [pricing, setPricing] = useState({});
- const [personalError, setPersonalError] = useState("");
- const [platformDetailsError, setPlatformDetailsError] = useState("");
- const [pricingError, setPricingError] = useState("");
- const [profilePic, setProfilePic] = useState(null);
- const [profilePicError, setProfilePicError] = useState("");
- const [submitError, setSubmitError] = useState("");
- const [submitSuccess, setSubmitSuccess] = useState("");
- const [submitting, setSubmitting] = useState(false);
- const [progress, setProgress] = useState(0);
- const [brandStep, setBrandStep] = useState(0);
- const [brandData, setBrandData] = useState(brandInitialState);
- const [brandLogoPreview, setBrandLogoPreview] = useState(null);
- const [brandError, setBrandError] = useState("");
-
- // Prefill name and email from Google user if available
- useEffect(() => {
- if (user) {
- setPersonal((prev) => ({
- ...prev,
- name: user.user_metadata?.name || prev.name,
- email: user.email || prev.email,
- }));
- }
- }, [user]);
-
- // Validation for personal details
- const validatePersonal = () => {
- if (!personal.name || personal.name.length < 2) return "Please enter a valid name.";
- if (!personal.email) return "Email is required.";
- if (!personal.age || isNaN(Number(personal.age)) || Number(personal.age) < 10 || Number(personal.age) > 99) return "Please enter a valid age (10-99).";
- if (!personal.gender) return "Please select a gender.";
- if (!personal.category) return "Please select a content category.";
- if (personal.category === "Other" && !personal.otherCategory) return "Please enter your content category.";
- if (!personal.country) return "Please enter a valid country.";
- return "";
- };
-
- // Validation for platform details
- const validatePlatformDetails = () => {
- for (const platform of selectedPlatforms) {
- const details = platformDetails[platform];
- if (!details) return `Please fill in all details for ${platform}.`;
- if (platform === "YouTube") {
- if (!details.channelUrl || !details.channelId || !details.channelName) return `Please provide a valid YouTube channel for ${platform}.`;
- } else {
- if (!details.profileUrl || !details.followers || !details.posts) return `Please fill in all details for ${platform}.`;
- if (isNaN(Number(details.followers)) || isNaN(Number(details.posts))) return `Followers and posts must be numbers for ${platform}.`;
- }
- }
- return "";
- };
-
- // Validation for pricing
- const validatePricing = () => {
- for (const platform of selectedPlatforms) {
- const p = pricing[platform];
- if (!p) return `Please fill in pricing for ${platform}.`;
- if (platform === "YouTube") {
- if (!p.per_video_cost || !p.per_short_cost || !p.per_community_post_cost || !p.currency) return `Please fill all YouTube pricing fields.`;
- if ([p.per_video_cost, p.per_short_cost, p.per_community_post_cost].some(v => isNaN(Number(v)))) return `YouTube pricing must be numbers.`;
- } else if (platform === "Instagram") {
- if (!p.per_post_cost || !p.per_story_cost || !p.per_reel_cost || !p.currency) return `Please fill all Instagram pricing fields.`;
- if ([p.per_post_cost, p.per_story_cost, p.per_reel_cost].some(v => isNaN(Number(v)))) return `Instagram pricing must be numbers.`;
- } else if (platform === "Facebook") {
- if (!p.per_post_cost || !p.currency) return `Please fill all Facebook pricing fields.`;
- if (isNaN(Number(p.per_post_cost))) return `Facebook pricing must be a number.`;
- } else if (platform === "TikTok") {
- if (!p.per_video_cost || !p.currency) return `Please fill all TikTok pricing fields.`;
- if (isNaN(Number(p.per_video_cost))) return `TikTok pricing must be a number.`;
- }
- }
- return "";
- };
-
- // Step 1: Role Selection
- const renderRoleStep = () => (
-
-
Are you a Brand or a Creator?
-
-
setRole("brand")}
- >
-
- Brand
-
-
setRole("creator")}
- >
-
- Content Creator
-
-
-
- );
-
- // Step 2: Personal Details
- const genderOptions = ["Male", "Female", "Non-binary", "Prefer not to say"];
- const categoryOptions = [
- "Tech",
- "Fashion",
- "Travel",
- "Food",
- "Fitness",
- "Beauty",
- "Gaming",
- "Education",
- "Music",
- "Finance",
- "Other",
- ];
- const renderPersonalStep = () => (
-
-
Personal Details
-
- {personalError &&
{personalError}
}
-
- );
-
- // Step 3: Platform Selection
- const renderPlatformStep = () => (
-
-
Which platforms do you use?
-
- {platforms.map((platform) => (
-
{
- setSelectedPlatforms((prev) =>
- prev.includes(platform.name)
- ? prev.filter((p) => p !== platform.name)
- : [...prev, platform.name]
- );
- }}
- className={`flex flex-col items-center px-6 py-4 rounded-xl border-2 transition-all duration-200 shadow-sm w-32 h-36 ${selectedPlatforms.includes(platform.name) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
- >
-
- {platform.name}
-
- ))}
-
-
- );
-
- // Step 4: Platform Details
- const renderPlatformDetailsStep = () => (
-
-
Platform Details
-
- {selectedPlatforms.map((platform) => (
-
-
-
p.name === platform)?.icon} alt={platform} className="h-8 w-8" />
-
{platform}
-
- {platform === "YouTube" && (
-
setPlatformDetails((prev: any) => ({ ...prev, [platform]: d }))}
- />
- )}
- {platform === "Instagram" && (
- setPlatformDetails((prev: any) => ({ ...prev, [platform]: d }))}
- />
- )}
- {platform === "Facebook" && (
- setPlatformDetails((prev: any) => ({ ...prev, [platform]: d }))}
- />
- )}
- {platform === "TikTok" && (
- setPlatformDetails((prev: any) => ({ ...prev, [platform]: d }))}
- />
- )}
-
- ))}
-
- {platformDetailsError &&
{platformDetailsError}
}
-
- );
-
- // Step 5: Pricing
- const renderPricingStep = () => (
-
-
Set Your Pricing
-
- {selectedPlatforms.map((platform) => (
-
-
-
p.name === platform)?.icon} alt={platform} className="h-8 w-8" />
-
{platform}
-
- {platform === "YouTube" && (
-
setPricing((prev: any) => ({ ...prev, [platform]: d }))}
- />
- )}
- {platform === "Instagram" && (
- setPricing((prev: any) => ({ ...prev, [platform]: d }))}
- />
- )}
- {platform === "Facebook" && (
- setPricing((prev: any) => ({ ...prev, [platform]: d }))}
- />
- )}
- {platform === "TikTok" && (
- setPricing((prev: any) => ({ ...prev, [platform]: d }))}
- />
- )}
-
- ))}
-
- {pricingError &&
{pricingError}
}
-
- );
-
- // Step 5: Profile Picture Upload (new step)
- const handleProfilePicChange = (e: React.ChangeEvent) => {
- setProfilePicError("");
- if (e.target.files && e.target.files[0]) {
- const file = e.target.files[0];
- if (file.size > 3 * 1024 * 1024) {
- setProfilePicError("File size must be less than 3MB.");
- setProfilePic(null);
- return;
- }
- setProfilePic(file);
- }
- };
-
- const renderProfilePicStep = () => (
-
-
Upload Profile Picture
-
-
- Choose File
-
-
-
- {(profilePic || user?.user_metadata?.avatar_url) ? (
-
- ) : (
-
No Image
- )}
- {profilePic &&
{profilePic.name}
}
-
- {profilePicError &&
{profilePicError}
}
-
Max file size: 3MB. You can skip this step if you want to use your Google/YouTube profile image.
-
-
- );
-
- // Step 6: Review & Submit
- const handleSubmit = async () => {
- setSubmitting(true);
- setSubmitError("");
- setSubmitSuccess("");
- setProgress(0);
- let profile_image_url = null;
- try {
- // 1. Upload profile picture if provided
- if (profilePic) {
- setProgress(20);
- const fileExt = profilePic.name.split('.').pop();
- const fileName = `${user?.id}_${Date.now()}.${fileExt}`;
- const { data, error } = await supabase.storage.from('profile-pictures').upload(fileName, profilePic);
- if (error) throw error;
- profile_image_url = `${supabase.storage.from('profile-pictures').getPublicUrl(fileName).data.publicUrl}`;
- } else if (user?.user_metadata?.avatar_url) {
- profile_image_url = user.user_metadata.avatar_url;
- }
- setProgress(40);
- // 2. Update users table
- const categoryToSave = personal.category === 'Other' ? personal.otherCategory : personal.category;
- const { error: userError } = await supabase.from('users').update({
- username: personal.name,
- age: personal.age,
- gender: personal.gender,
- country: personal.country,
- category: categoryToSave,
- profile_image: profile_image_url,
- role,
- }).eq('id', user?.id);
- if (userError) throw userError;
- setProgress(60);
- // 3. Insert social_profiles for each platform
- for (const platform of selectedPlatforms) {
- const details = platformDetails[platform];
- const p = pricing[platform];
- const profileData: any = {
- user_id: user?.id,
- platform,
- per_post_cost: p?.per_post_cost ? Number(p.per_post_cost) : null,
- per_story_cost: p?.per_story_cost ? Number(p.per_story_cost) : null,
- per_reel_cost: p?.per_reel_cost ? Number(p.per_reel_cost) : null,
- per_video_cost: p?.per_video_cost ? Number(p.per_video_cost) : null,
- per_short_cost: p?.per_short_cost ? Number(p.per_short_cost) : null,
- per_community_post_cost: p?.per_community_post_cost ? Number(p.per_community_post_cost) : null,
- per_post_cost_currency: p?.currency || null,
- per_story_cost_currency: p?.currency || null,
- per_reel_cost_currency: p?.currency || null,
- per_video_cost_currency: p?.currency || null,
- per_short_cost_currency: p?.currency || null,
- per_community_post_cost_currency: p?.currency || null,
- };
- if (platform === 'YouTube') {
- Object.assign(profileData, {
- channel_id: details.channelId,
- channel_name: details.channelName,
- profile_image: details.profile_image,
- subscriber_count: details.subscriber_count ? Number(details.subscriber_count) : null,
- total_views: details.total_views ? Number(details.total_views) : null,
- video_count: details.video_count ? Number(details.video_count) : null,
- channel_url: details.channelUrl,
- });
- } else {
- Object.assign(profileData, {
- username: details.profileUrl,
- followers: details.followers ? Number(details.followers) : null,
- posts: details.posts ? Number(details.posts) : null,
- profile_image: null,
- channel_url: details.profileUrl,
- });
- }
- // Upsert to avoid duplicates
- const { error: spError } = await supabase.from('social_profiles').upsert(profileData, { onConflict: 'user_id,platform' });
- if (spError) throw spError;
- }
- setProgress(90);
- setSubmitSuccess('Onboarding complete! Your details have been saved.');
- setProgress(100);
- // Route based on role
- if (role === "brand") {
- setTimeout(() => navigate('/brand/dashboard'), 1200);
- } else {
- setTimeout(() => navigate('/dashboard'), 1200);
- }
- } catch (err: any) {
- setSubmitError(err.message || 'Failed to submit onboarding data.');
- setProgress(0);
- } finally {
- setSubmitting(false);
- }
- };
-
- const renderReviewStep = () => (
-
-
Review & Submit
- {submitting && (
-
- )}
-
-
Profile Picture
-
- {(profilePic || user?.user_metadata?.avatar_url) ? (
-
- ) : (
-
No Image
- )}
- {profilePic &&
{profilePic.name}
}
-
-
-
-
Personal Details
-
- Name: {personal.name}
- Email: {personal.email}
- Age: {personal.age}
- Gender: {personal.gender}
- Country: {personal.country}
- Category: {personal.category === 'Other' ? personal.otherCategory : personal.category}
-
-
-
-
Platforms
- {selectedPlatforms.map(platform => (
-
-
{platform}
-
- {platform === 'YouTube' ? (
- <>
- Channel Name: {platformDetails[platform]?.channelName}
- Subscribers: {platformDetails[platform]?.subscriber_count}
- Videos: {platformDetails[platform]?.video_count}
- Views: {platformDetails[platform]?.total_views}
- Channel URL: {platformDetails[platform]?.channelUrl}
- Pricing: Video: {pricing[platform]?.per_video_cost}, Short: {pricing[platform]?.per_short_cost}, Community Post: {pricing[platform]?.per_community_post_cost} ({pricing[platform]?.currency})
- >
- ) : (
- <>
- Profile URL: {platformDetails[platform]?.profileUrl}
- Followers: {platformDetails[platform]?.followers}
- Posts: {platformDetails[platform]?.posts}
- Pricing: {platform === 'Instagram' ? `Post: ${pricing[platform]?.per_post_cost}, Story: ${pricing[platform]?.per_story_cost}, Reel: ${pricing[platform]?.per_reel_cost}` : `Post/Video: ${pricing[platform]?.per_post_cost || pricing[platform]?.per_video_cost}`} ({pricing[platform]?.currency})
- >
- )}
-
-
- ))}
-
- {submitError &&
{submitError}
}
- {submitSuccess &&
{submitSuccess}
}
-
- );
-
- const handleNext = () => {
- if (step === 1) {
- const err = validatePersonal();
- if (err) {
- setPersonalError(err);
- return;
- } else {
- setPersonalError("");
- }
- }
- if (step === 3) {
- const err = validatePlatformDetails();
- if (err) {
- setPlatformDetailsError(err);
- return;
- } else {
- setPlatformDetailsError("");
- }
- }
- if (step === 4) {
- const err = validatePricing();
- if (err) {
- setPricingError(err);
- return;
- } else {
- setPricingError("");
- }
- }
- if (step < steps.length - 1) setStep(step + 1);
- };
- const handleBack = () => {
- if (step > 0) setStep(step - 1);
- };
-
- // Brand onboarding steps
- const brandSteps = [
- "Brand Details",
- "Contact Information",
- "Platforms",
- "Social Links",
- "Collaboration Preferences",
- "Review & Submit",
- ];
-
- // Brand Step 1: Brand Details
- const companySizes = ["1-10", "11-50", "51-200", "201-1000", "1000+"];
- const industries = ["Tech", "Fashion", "Travel", "Food", "Fitness", "Beauty", "Gaming", "Education", "Music", "Finance", "Other"];
- const handleBrandLogoChange = (e: React.ChangeEvent) => {
- if (e.target.files && e.target.files[0]) {
- setBrandData({ ...brandData, logo: e.target.files[0] });
- setBrandLogoPreview(URL.createObjectURL(e.target.files[0]));
- }
- };
- const renderBrandDetailsStep = () => (
-
- );
-
- // Brand Step 2: Contact Information
- const renderBrandContactStep = () => (
-
-
Contact Information
- setBrandData({ ...brandData, contact_person: e.target.value })}
- className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
- />
- setBrandData({ ...brandData, contact_email: e.target.value })}
- className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
- />
- setBrandData({ ...brandData, contact_phone: e.target.value })}
- className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
- />
- setBrandData({ ...brandData, role: e.target.value })}
- className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
- />
-
- );
-
- // Brand Step 3: Platforms
- const allBrandPlatforms = [
- { name: "Instagram", key: "instagram_url" },
- { name: "YouTube", key: "youtube_url" },
- { name: "Facebook", key: "facebook_url" },
- { name: "Twitter", key: "twitter_url" },
- { name: "LinkedIn", key: "linkedin_url" },
- // Add TikTok if needed
- ];
- const renderBrandPlatformsStep = () => (
-
-
Which platforms is your brand on?
-
- {allBrandPlatforms.map(platform => (
- {
- setBrandData(prev => {
- const exists = prev.platforms.includes(platform.name);
- return {
- ...prev,
- platforms: exists
- ? prev.platforms.filter(p => p !== platform.name)
- : [...prev.platforms, platform.name],
- };
- });
- }}
- className={`px-6 py-3 rounded-lg border-2 font-semibold ${brandData.platforms.includes(platform.name) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
- >
- {platform.name}
-
- ))}
-
-
- );
-
- // Brand Step 4: Social Links (conditional)
- const socialLinkExamples: Record = {
- instagram_url: "https://instagram.com/yourbrand",
- youtube_url: "https://youtube.com/yourbrand",
- facebook_url: "https://facebook.com/yourbrand",
- twitter_url: "https://twitter.com/yourbrand",
- linkedin_url: "https://linkedin.com/company/yourbrand",
- };
- const renderBrandSocialLinksStep = () => (
-
-
Social Links
- {brandData.platforms.map(platform => {
- const key = allBrandPlatforms.find(p => p.name === platform)?.key;
- if (!key) return null;
- return (
-
- {platform} URL
- setBrandData({
- ...brandData,
- social_links: { ...brandData.social_links, [key]: e.target.value },
- })}
- className="w-full px-4 py-3 rounded-lg border border-gray-300"
- />
-
- );
- })}
-
- );
-
- // Brand Step 5: Collaboration Preferences
- const collabTypes = ["Sponsored Posts", "Giveaways", "Product Reviews", "Long-term Partnerships", "Affiliate Marketing", "Events", "Content Creation", "Brand Ambassadorship", "Social Media Takeover", "Other"];
- const creatorCategories = ["Tech", "Fashion", "Travel", "Food", "Fitness", "Beauty", "Gaming", "Education", "Music", "Finance", "Other"];
- const brandValues = ["Sustainability", "Innovation", "Diversity", "Quality", "Community", "Transparency", "Customer Focus", "Creativity", "Integrity", "Other"];
- const tones = ["Professional", "Friendly", "Humorous", "Inspirational", "Bold", "Casual", "Formal", "Playful", "Serious", "Other"];
- const toggleMultiSelect = (field: keyof BrandData, value: string) => {
- setBrandData(prev => {
- const arr = prev[field] as string[];
- return {
- ...prev,
- [field]: arr.includes(value) ? arr.filter(v => v !== value) : [...arr, value],
- };
- });
- };
- const renderBrandCollabPrefsStep = () => (
-
-
Collaboration Preferences
-
-
Collaboration Types
-
- {collabTypes.map(type => (
- toggleMultiSelect("collaboration_types", type)}
- className={`px-4 py-2 rounded-lg border-2 text-sm ${brandData.collaboration_types.includes(type) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
- >
- {type}
-
- ))}
-
-
-
-
Preferred Creator Categories
-
- {creatorCategories.map(cat => (
- toggleMultiSelect("preferred_creator_categories", cat)}
- className={`px-4 py-2 rounded-lg border-2 text-sm ${brandData.preferred_creator_categories.includes(cat) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
- >
- {cat}
-
- ))}
-
-
-
-
Brand Values
-
- {brandValues.map(val => (
- toggleMultiSelect("brand_values", val)}
- className={`px-4 py-2 rounded-lg border-2 text-sm ${brandData.brand_values.includes(val) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
- >
- {val}
-
- ))}
-
-
-
-
Preferred Tone
-
- {tones.map(tone => (
- toggleMultiSelect("preferred_tone", tone)}
- className={`px-4 py-2 rounded-lg border-2 text-sm ${brandData.preferred_tone.includes(tone) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
- >
- {tone}
-
- ))}
-
-
-
- );
-
- // Brand step validation
- const validateBrandStep = () => {
- if (brandStep === 0) {
- if (!brandData.brand_name) return "Brand name is required.";
- if (!brandData.website_url) return "Website URL is required.";
- if (!brandData.industry) return "Industry is required.";
- if (!brandData.company_size) return "Company size is required.";
- if (!brandData.location) return "Location is required.";
- if (!brandData.description) return "Description is required.";
- }
- if (brandStep === 1) {
- if (!brandData.contact_person) return "Contact person is required.";
- if (!brandData.contact_email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(brandData.contact_email)) return "Valid contact email is required.";
- }
- if (brandStep === 2) {
- if (!brandData.platforms.length) return "Select at least one platform.";
- }
- if (brandStep === 3) {
- for (const platform of brandData.platforms) {
- const key = allBrandPlatforms.find(p => p.name === platform)?.key;
- if (key && !brandData.social_links[key]) return `Enter your ${platform} URL.`;
- if (key && brandData.social_links[key] && !/^https?:\/\//.test(brandData.social_links[key])) return `${platform} URL must start with http:// or https://`;
- }
- }
- if (brandStep === 4) {
- if (!brandData.collaboration_types.length) return "Select at least one collaboration type.";
- if (!brandData.preferred_creator_categories.length) return "Select at least one creator category.";
- if (!brandData.brand_values.length) return "Select at least one brand value.";
- if (!brandData.preferred_tone.length) return "Select at least one preferred tone.";
- }
- return "";
- };
- const handleBrandNext = () => {
- const err = validateBrandStep();
- if (err) {
- setBrandError(err);
- return;
- } else {
- setBrandError("");
- }
- if (brandStep < brandSteps.length - 1) setBrandStep(brandStep + 1);
- };
- const handleBrandBack = () => {
- if (brandStep > 0) setBrandStep(brandStep - 1);
- };
-
- // Brand Step 6: Review & Submit
- const [brandSubmitting, setBrandSubmitting] = useState(false);
- const [brandSubmitError, setBrandSubmitError] = useState("");
- const [brandSubmitSuccess, setBrandSubmitSuccess] = useState("");
- const handleBrandSubmit = async () => {
- setBrandSubmitting(true);
- setBrandSubmitError("");
- setBrandSubmitSuccess("");
- let logo_url = null;
- try {
- // 1. Upload logo if provided
- if (brandData.logo) {
- const fileExt = brandData.logo.name.split('.').pop();
- const fileName = `${user?.id}_${Date.now()}.${fileExt}`;
- const { data, error } = await supabase.storage.from('brand-logos').upload(fileName, brandData.logo);
- if (error) throw error;
- logo_url = supabase.storage.from('brand-logos').getPublicUrl(fileName).data.publicUrl;
- }
- // 2. Insert into brands table
- const { error: brandError } = await supabase.from('brands').insert({
- user_id: user?.id,
- brand_name: brandData.brand_name,
- logo_url,
- website_url: brandData.website_url,
- industry: brandData.industry,
- company_size: brandData.company_size,
- location: brandData.location,
- description: brandData.description,
- contact_person: brandData.contact_person,
- contact_email: brandData.contact_email,
- contact_phone: brandData.contact_phone,
- role: brandData.role,
- instagram_url: brandData.social_links.instagram_url || null,
- facebook_url: brandData.social_links.facebook_url || null,
- twitter_url: brandData.social_links.twitter_url || null,
- linkedin_url: brandData.social_links.linkedin_url || null,
- youtube_url: brandData.social_links.youtube_url || null,
- collaboration_types: brandData.collaboration_types,
- preferred_creator_categories: brandData.preferred_creator_categories,
- brand_values: brandData.brand_values,
- preferred_tone: brandData.preferred_tone,
- platforms: brandData.platforms,
- });
- if (brandError) throw brandError;
- setBrandSubmitSuccess("Brand onboarding complete! Redirecting to dashboard...");
- // Clear localStorage for brand onboarding
- localStorage.removeItem("brandStep");
- localStorage.removeItem("brandData");
- setTimeout(() => navigate("/brand/dashboard"), 1200);
- } catch (err: any) {
- setBrandSubmitError(err.message || "Failed to submit brand onboarding data.");
- } finally {
- setBrandSubmitting(false);
- }
- };
- const renderBrandReviewStep = () => (
-
-
Review & Submit
-
-
Logo
- {(brandLogoPreview || brandData.logo) ? (
-
- ) : (
-
No Logo
- )}
-
-
-
Brand Details
-
- Name: {brandData.brand_name}
- Website: {brandData.website_url}
- Industry: {brandData.industry}
- Company Size: {brandData.company_size}
- Location: {brandData.location}
- Description: {brandData.description}
-
-
-
-
Contact Information
-
- Contact Person: {brandData.contact_person}
- Email: {brandData.contact_email}
- Phone: {brandData.contact_phone}
- Role: {brandData.role}
-
-
-
-
Platforms & Social Links
-
- {brandData.platforms.map(platform => {
- const key = allBrandPlatforms.find(p => p.name === platform)?.key;
- return (
- {platform}: {key ? brandData.social_links[key] : ""}
- );
- })}
-
-
-
-
Collaboration Preferences
-
- Collaboration Types: {brandData.collaboration_types.join(", ")}
- Preferred Creator Categories: {brandData.preferred_creator_categories.join(", ")}
- Brand Values: {brandData.brand_values.join(", ")}
- Preferred Tone: {brandData.preferred_tone.join(", ")}
-
-
- {brandSubmitError &&
{brandSubmitError}
}
- {brandSubmitSuccess &&
{brandSubmitSuccess}
}
-
- {brandSubmitting ? 'Submitting...' : 'Submit'}
-
-
- );
-
- // Persist and restore brand onboarding state
- useEffect(() => {
- const savedStep = localStorage.getItem("brandStep");
- const savedData = localStorage.getItem("brandData");
- if (savedStep) setBrandStep(Number(savedStep));
- if (savedData) setBrandData(JSON.parse(savedData));
- }, []);
- useEffect(() => {
- localStorage.setItem("brandStep", String(brandStep));
- localStorage.setItem("brandData", JSON.stringify(brandData));
- }, [brandStep, brandData]);
-
- return (
-
-
- {/* Stepper UI */}
-
- {role === "brand"
- ? brandSteps.map((label, idx) => (
-
{label}
- ))
- : steps.map((label, idx) => (
-
{label}
- ))}
-
- {/* Step Content */}
-
- {role === "brand" ? (
- <>
- {brandStep === 0 && renderBrandDetailsStep()}
- {brandStep === 1 && renderBrandContactStep()}
- {brandStep === 2 && renderBrandPlatformsStep()}
- {brandStep === 3 && renderBrandSocialLinksStep()}
- {brandStep === 4 && renderBrandCollabPrefsStep()}
- {brandStep === 5 && renderBrandReviewStep()}
- >
- ) : (
- <>
- {step === 0 && renderRoleStep()}
- {step === 1 && renderPersonalStep()}
- {step === 2 && renderPlatformStep()}
- {step === 3 && renderPlatformDetailsStep()}
- {step === 4 && renderPricingStep()}
- {step === 5 && renderProfilePicStep()}
- {step === 6 && renderReviewStep()}
- >
- )}
-
- {/* Navigation */}
-
- {role === "brand" ? (
- <>
-
- Back
-
- {brandStep < brandSteps.length - 1 ? (
-
- Next
-
- ) : null}
- >
- ) : (
- <>
-
- Back
-
- {step < steps.length - 1 ? (
-
- Next
-
- ) : (
-
- {submitting ? 'Submitting...' : 'Submit'}
-
- )}
- >
- )}
-
- {brandError &&
{brandError}
}
-
-
- );
-}
-
-// Platform detail components
-function YouTubeDetails({ details, setDetails }: { details: any, setDetails: (d: any) => void }) {
- const [input, setInput] = useState(details.channelUrl || "");
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState("");
- const [showInfo, setShowInfo] = useState(false);
-
- const fetchChannel = async () => {
- setLoading(true);
- setError("");
- let channelId = input;
- // Extract channel ID from URL if needed
- if (input.includes("youtube.com")) {
- const match = input.match(/(?:channel\/|user\/|c\/)?([\w-]{21,})/);
- if (match) channelId = match[1];
- }
- try {
- const res = await fetch(
- `/youtube/channel-info?channelId=${encodeURIComponent(channelId)}`
- );
- if (!res.ok) {
- let errMsg = `Error: ${res.status}`;
- try {
- const errData = await res.json();
- if (errData && errData.detail) errMsg = errData.detail;
- } catch {}
- setError(errMsg);
- return;
- }
- const data = await res.json();
- if (data.items && data.items.length > 0) {
- const ch = data.items[0];
- setDetails({
- channelUrl: input,
- channelId: ch.id,
- channelName: ch.snippet.title,
- profile_image: ch.snippet.thumbnails.default.url,
- subscriber_count: ch.statistics.subscriberCount,
- total_views: ch.statistics.viewCount,
- video_count: ch.statistics.videoCount,
- });
- } else {
- setError("Channel not found");
- }
- } catch (e) {
- setError("Failed to fetch channel. Please check your network connection or try again later.");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
- YouTube Channel URL or ID
- setShowInfo(true)}
- className="ml-1 text-purple-600 hover:text-purple-800 focus:outline-none"
- aria-label="How to find your YouTube channel URL or ID"
- >
-
-
-
-
setInput(e.target.value)}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="e.g. https://www.youtube.com/channel/UCxxxxxxxxxxxxxxxxxxx or channel ID"
- />
- {/* Info Dialog */}
- {showInfo && (
-
-
-
setShowInfo(false)}
- aria-label="Close"
- >
- ×
-
-
How to find your YouTube Channel URL or ID
-
- Go to youtube.com and sign in.
- Click your profile picture at the top right and select Your Channel .
- Click Customize Channel (top right).
- Go to the Basic info tab.
- Find the Channel URL section and copy the URL shown there.
- Paste the full Channel URL above (e.g. https://www.youtube.com/channel/UCxxxxxxxxxxxxxxxxxxx ).
-
-
-
- )}
-
- {loading ? "Fetching..." : "Fetch Channel"}
-
- {error &&
{error}
}
- {details.channelName && (
-
-
Name: {details.channelName}
-
Subscribers: {details.subscriber_count}
-
Videos: {details.video_count}
-
Views: {details.total_views}
-
- )}
-
- );
-}
-
-function InstagramDetails({ details, setDetails }: { details: any, setDetails: (d: any) => void }) {
- return (
-
- Instagram Profile URL
- setDetails({ ...details, profileUrl: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Paste your Instagram profile URL"
- />
- Followers
- setDetails({ ...details, followers: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Followers count"
- />
- Posts
- setDetails({ ...details, posts: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Number of posts"
- />
-
- );
-}
-
-function FacebookDetails({ details, setDetails }: { details: any, setDetails: (d: any) => void }) {
- return (
-
- Facebook Profile URL
- setDetails({ ...details, profileUrl: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Paste your Facebook profile URL"
- />
- Followers
- setDetails({ ...details, followers: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Followers count"
- />
- Posts
- setDetails({ ...details, posts: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Number of posts"
- />
-
- );
-}
-
-function TikTokDetails({ details, setDetails }: { details: any, setDetails: (d: any) => void }) {
- return (
-
- TikTok Profile URL
- setDetails({ ...details, profileUrl: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Paste your TikTok profile URL"
- />
- Followers
- setDetails({ ...details, followers: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Followers count"
- />
- Posts
- setDetails({ ...details, posts: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Number of posts"
- />
-
- );
-}
-
-// Pricing components
-function YouTubePricing({ pricing, setPricing }: { pricing: any, setPricing: (d: any) => void }) {
- return (
-
- Per Video
- setPricing({ ...pricing, per_video_cost: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Price per video"
- />
- Per Short
- setPricing({ ...pricing, per_short_cost: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Price per short"
- />
- Per Community Post
- setPricing({ ...pricing, per_community_post_cost: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Price per community post"
- />
- Currency
- setPricing({ ...pricing, currency: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="e.g. USD, INR"
- />
-
- );
-}
-
-function InstagramPricing({ pricing, setPricing }: { pricing: any, setPricing: (d: any) => void }) {
- return (
-
- Per Post
- setPricing({ ...pricing, per_post_cost: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Price per post"
- />
- Per Story
- setPricing({ ...pricing, per_story_cost: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Price per story"
- />
- Per Reel
- setPricing({ ...pricing, per_reel_cost: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Price per reel"
- />
- Currency
- setPricing({ ...pricing, currency: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="e.g. USD, INR"
- />
-
- );
-}
-
-function FacebookPricing({ pricing, setPricing }: { pricing: any, setPricing: (d: any) => void }) {
- return (
-
- Per Post
- setPricing({ ...pricing, per_post_cost: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Price per post"
- />
- Currency
- setPricing({ ...pricing, currency: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="e.g. USD, INR"
- />
-
- );
-}
-
-function TikTokPricing({ pricing, setPricing }: { pricing: any, setPricing: (d: any) => void }) {
- return (
-
- Per Video
- setPricing({ ...pricing, per_video_cost: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="Price per video"
- />
- Currency
- setPricing({ ...pricing, currency: e.target.value })}
- className="w-full px-4 py-2 rounded border border-gray-300"
- placeholder="e.g. USD, INR"
- />
-
- );
-}
diff --git a/Frontend/src/components/ProtectedRoute.tsx b/Frontend/src/components/ProtectedRoute.tsx
deleted file mode 100644
index 5f9564f..0000000
--- a/Frontend/src/components/ProtectedRoute.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Navigate } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-
-const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
- const { isAuthenticated } = useAuth();
-
- return isAuthenticated ? children : ;
-};
-
-export default ProtectedRoute;
diff --git a/Frontend/src/components/PublicRoute.tsx b/Frontend/src/components/PublicRoute.tsx
deleted file mode 100644
index 7bb29a7..0000000
--- a/Frontend/src/components/PublicRoute.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Navigate } from "react-router-dom";
-import { useAuth } from "../context/AuthContext";
-import { useState, useEffect } from "react";
-
-interface PublicRouteProps {
- children: React.ReactNode;
- redirectTo?: string;
-}
-
-const PublicRoute = ({ children, redirectTo = "/dashboard" }: PublicRouteProps) => {
- const { isAuthenticated, user, checkUserOnboarding } = useAuth();
- const [redirectPath, setRedirectPath] = useState(null);
- const [isChecking, setIsChecking] = useState(false);
-
- useEffect(() => {
- if (isAuthenticated && user && !isChecking) {
- // Add a simple cache to prevent repeated checks
- const cacheKey = `onboarding_check_${user.id}`;
- const cachedResult = sessionStorage.getItem(cacheKey);
-
- if (cachedResult) {
- const { hasOnboarding, role } = JSON.parse(cachedResult);
- if (hasOnboarding) {
- if (role === "brand") {
- setRedirectPath("/brand/dashboard");
- } else {
- setRedirectPath("/dashboard");
- }
- } else {
- setRedirectPath("/onboarding");
- }
- return;
- }
-
- setIsChecking(true);
- console.log("PublicRoute: Checking user onboarding status");
- checkUserOnboarding(user).then(({ hasOnboarding, role }) => {
- console.log("PublicRoute: Onboarding check result", { hasOnboarding, role });
-
- // Cache the result for 2 minutes
- sessionStorage.setItem(cacheKey, JSON.stringify({ hasOnboarding, role }));
- setTimeout(() => sessionStorage.removeItem(cacheKey), 2 * 60 * 1000);
-
- if (hasOnboarding) {
- if (role === "brand") {
- setRedirectPath("/brand/dashboard");
- } else {
- setRedirectPath("/dashboard");
- }
- } else {
- setRedirectPath("/onboarding");
- }
- setIsChecking(false);
- }).catch(error => {
- console.error("PublicRoute: Error checking onboarding", error);
- setIsChecking(false);
- });
- }
- }, [isAuthenticated, user, checkUserOnboarding, isChecking]);
-
- if (redirectPath) {
- console.log("PublicRoute: Redirecting to", redirectPath);
- return ;
- }
-
- return <>{children}>;
-};
-
-export default PublicRoute;
\ No newline at end of file
diff --git a/Frontend/src/components/analytics/audience-metrics.tsx b/Frontend/src/components/analytics/audience-metrics.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/Frontend/src/components/analytics/campaign-comparison.tsx b/Frontend/src/components/analytics/campaign-comparison.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/Frontend/src/components/analytics/performance-overview.tsx b/Frontend/src/components/analytics/performance-overview.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/Frontend/src/components/analytics/revenue-analytics.tsx b/Frontend/src/components/analytics/revenue-analytics.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/Frontend/src/components/chat/chat-item.tsx b/Frontend/src/components/chat/chat-item.tsx
deleted file mode 100644
index bd575a7..0000000
--- a/Frontend/src/components/chat/chat-item.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { Chat } from "@/redux/chatSlice";
-import React, { useEffect } from "react";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
-import { cn } from "@/lib/utils";
-import { useSelector } from "react-redux";
-import { RootState } from "@/redux/store";
-import { useChat } from "@/lib/useChat";
-import { formatDistanceToNow } from "date-fns";
-
-function formatChatDate(lastSeen: string | null) {
- if (!lastSeen) return "";
- const date = new Date(lastSeen);
- return formatDistanceToNow(date, { addSuffix: true });
-}
-
-export default function ChatItem({
- chat,
- handleChatClick,
-}: {
- chat: Chat;
- handleChatClick: (chatId: string) => void;
-}) {
- const selectedChatId = useSelector(
- (state: RootState) => state.chat.selectedChatId
- );
-
- const lastMessage = useSelector((state: RootState) =>
- chat.messageIds.length
- ? state.chat.messages[chat.messageIds[chat.messageIds.length - 1]].message
- : null
- );
-
- const { fetchUserDetails } = useChat();
-
- useEffect(() => {
- if (!chat.receiver.username) {
- fetchUserDetails(chat.receiver.id, chat.id);
- }
- }, [chat.receiver.username]);
-
- if (!chat.receiver.username) return null;
-
- return (
- handleChatClick(chat.id)}
- >
-
-
-
- {chat.receiver.username?.[0] || "U"}
-
-
-
-
- {chat.receiver.username}
-
-
- {formatChatDate(chat.lastMessageTime)}
-
-
-
{lastMessage}
-
-
-
- );
-}
diff --git a/Frontend/src/components/chat/chat-list.tsx b/Frontend/src/components/chat/chat-list.tsx
deleted file mode 100644
index 19a1431..0000000
--- a/Frontend/src/components/chat/chat-list.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-"use client";
-import { useEffect, useState } from "react";
-import { Input } from "../ui/input";
-import { useDispatch, useSelector } from "react-redux";
-import { RootState } from "@/redux/store";
-import { Chat, setSelectedChat } from "@/redux/chatSlice";
-
-import { useChat } from "@/lib/useChat";
-import ChatItem from "./chat-item";
-import { CreateNewChat } from "./create-new-chat";
-import ChatSearch from "./chat-search";
-
-export default function ChatList() {
- const chats = useSelector((state: RootState) => state.chat.chats);
- const [sortedChatList, setSortedChatList] = useState([]);
- const dispatch = useDispatch();
- const { fetchChatList } = useChat();
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- setLoading(true);
- fetchChatList().finally(() => {
- setLoading(false);
- });
- }, []);
-
- useEffect(() => {
- const sortedList = Object.values(chats).sort((a, b) => {
- return (
- new Date(b.lastMessageTime).getTime() -
- new Date(a.lastMessageTime).getTime()
- );
- });
- setSortedChatList(sortedList);
- }, [chats]);
-
- const handleChatClick = (chatId: string) => {
- dispatch(setSelectedChat(chatId));
- };
-
- return (
-
-
-
-
-
-
- {loading && (
-
- )}
- {!loading && sortedChatList.length === 0 && (
-
- )}
- {sortedChatList.map((chat) => (
-
- ))}
-
-
- );
-}
diff --git a/Frontend/src/components/chat/chat-search.tsx b/Frontend/src/components/chat/chat-search.tsx
deleted file mode 100644
index 7ef2366..0000000
--- a/Frontend/src/components/chat/chat-search.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useDispatch, useSelector } from "react-redux";
-import { RootState } from "@/redux/store";
-import { setSelectedChat } from "@/redux/chatSlice";
-import { Input } from "@/components/ui/input";
-import { Search as SearchIcon } from "lucide-react";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { Button } from "@/components/ui/button";
-
-const ChatSearch = () => {
- const dispatch = useDispatch();
- const chats = useSelector((state: RootState) => state.chat.chats);
- const messages = useSelector((state: RootState) => state.chat.messages);
-
- const [isOpen, setIsOpen] = useState(false);
- const [searchQuery, setSearchQuery] = useState("");
- const [searchResults, setSearchResults] = useState([]);
-
- useEffect(() => {
- if (searchQuery.trim() === "") {
- setSearchResults([]);
- return;
- }
-
- const results: any[] = [];
- const query = searchQuery.toLowerCase();
-
- // Search through chats for username matches
- Object.values(chats).forEach((chat) => {
- if (chat.receiver.username?.toLowerCase().includes(query)) {
- results.push({
- type: "chat",
- chatId: chat.id,
- username: chat.receiver.username,
- profileImage: chat.receiver.profileImage,
- });
- }
-
- // Search through messages in this chat
- const chatMessages = chat.messageIds
- .map((id) => messages[id])
- .filter((message) => message?.message.toLowerCase().includes(query));
-
- chatMessages.forEach((message) => {
- results.push({
- type: "message",
- chatId: chat.id,
- messageId: message.id,
- messagePreview: message.message,
- username: chat.receiver.username,
- profileImage: chat.receiver.profileImage,
- });
- });
- });
-
- setSearchResults(results);
- }, [searchQuery, chats, messages]);
-
- const handleResultClick = (chatId: string) => {
- dispatch(setSelectedChat(chatId));
- setIsOpen(false);
- setSearchQuery("");
- };
-
- const renderSearchResults = () => {
- if (searchResults.length === 0 && searchQuery.trim() !== "") {
- return (
-
- No results found
-
- );
- }
-
- return searchResults.map((result, index) => (
- handleResultClick(result.chatId)}
- >
-
-
-
-
- {result.username ? result.username.charAt(0).toUpperCase() : "?"}
-
-
-
- {result.username || "Unknown"}
- {result.type === "message" && (
-
- {result.messagePreview}
-
- )}
-
-
-
- ));
- };
-
- return (
-
-
-
-
- {
- setSearchQuery(e.target.value);
- if (e.target.value.trim() !== "") {
- setIsOpen(true);
- }
- }}
- onFocus={() => {
- if (searchQuery.trim() !== "") {
- setIsOpen(true);
- }
- }}
- />
-
-
- e.preventDefault()} // This prevents the auto focus
- >
- {renderSearchResults()}
-
-
- );
-};
-
-export default ChatSearch;
diff --git a/Frontend/src/components/chat/chat.tsx b/Frontend/src/components/chat/chat.tsx
deleted file mode 100644
index e177e68..0000000
--- a/Frontend/src/components/chat/chat.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { ChatProvider } from "@/lib/useChat";
-import ChatList from "./chat-list";
-import MessagesView from "./messages-view";
-import { useState } from "react";
-import { Input } from "../ui/input";
-import { Button } from "../ui/button";
-
-export default function Chat() {
- const [inputUserId, setInputUserId] = useState(null);
- const [userId, setUserId] = useState(null);
- return (
- <>
-
- setInputUserId(e.target.value)}
- placeholder="Enter user ID"
- className="mb-4 max-w-xl ml-auto"
- disabled={!!userId}
- />
- {
- setUserId(inputUserId);
- }}
- className="absolute right-2 top-1"
- >
- {userId ? "Connected" : "Connect"}
-
-
- {userId && (
-
-
-
-
-
-
- )}
- >
- );
-}
diff --git a/Frontend/src/components/chat/create-new-chat.tsx b/Frontend/src/components/chat/create-new-chat.tsx
deleted file mode 100644
index 603b059..0000000
--- a/Frontend/src/components/chat/create-new-chat.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-"use client";
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import { CirclePlus } from "lucide-react";
-import { useChat } from "@/lib/useChat";
-
-export function CreateNewChat() {
- const [open, setOpen] = useState(false);
- const [loading, setLoading] = useState(false);
- const [username, setUsername] = useState("");
- const [message, setMessage] = useState("");
- const { createChatWithMessage } = useChat();
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- if (!username.trim() || !message.trim()) {
- return;
- }
-
- setLoading(true);
-
- createChatWithMessage(username, message)
- .then((success) => {
- if (success) {
- setUsername("");
- setMessage("");
- } else {
- // Handle error
- console.error("Failed to create chat");
- }
- })
- .finally(() => {
- setOpen(false);
- setLoading(false);
- });
- };
-
- return (
-
-
-
-
-
-
-
-
- );
-}
diff --git a/Frontend/src/components/chat/message-input.tsx b/Frontend/src/components/chat/message-input.tsx
deleted file mode 100644
index e21d333..0000000
--- a/Frontend/src/components/chat/message-input.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React, { useState } from "react";
-import { Input } from "../ui/input";
-import { Button } from "../ui/button";
-import { Send } from "lucide-react";
-import { useSelector } from "react-redux";
-import { RootState } from "@/redux/store";
-import { useChat } from "@/lib/useChat";
-
-export default function MessageInput({
- containerRef,
-}: {
- containerRef: React.RefObject;
-}) {
- const [message, setMessage] = useState("");
- const receiverId = useSelector(
- (state: RootState) =>
- state.chat.chats[state.chat.selectedChatId!].receiver.id
- );
- const { sendMessage } = useChat();
- const handleSendMessage = () => {
- if (message.trim() === "") return;
- sendMessage(receiverId, message);
- setMessage("");
- // Scroll to the bottom of the container
- setTimeout(() => {
- if (containerRef.current) {
- containerRef.current.scrollTop = containerRef.current.scrollHeight;
- }
- }, 1000);
- };
- const handleKeyDown = (event: React.KeyboardEvent) => {
- if (event.key === "Enter") {
- event.preventDefault();
- handleSendMessage();
- }
- };
- return (
-
-
- setMessage(e.target.value)}
- onKeyDown={handleKeyDown}
- />
-
-
-
-
-
- );
-}
diff --git a/Frontend/src/components/chat/message-item.tsx b/Frontend/src/components/chat/message-item.tsx
deleted file mode 100644
index 3082604..0000000
--- a/Frontend/src/components/chat/message-item.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { type Message } from "@/redux/chatSlice";
-import { CheckCheckIcon, CheckIcon } from "lucide-react";
-
-import React from "react";
-
-export default function MessageItem({ message }: { message: Message }) {
- return (
- <>
- {message.isSent ? (
-
-
-
{message.message}
-
-
- {new Date(message.createdAt).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- })}
-
- {message.status === "sent" && (
-
-
-
- )}
- {message.status === "delivered" && (
-
-
-
- )}
- {message.status === "seen" && (
-
-
-
- )}
-
-
-
- ) : (
-
-
-
{message.message}
-
-
- {new Date(message.createdAt).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- })}
-
-
-
-
- )}
- >
- );
-}
diff --git a/Frontend/src/components/chat/messages-list.tsx b/Frontend/src/components/chat/messages-list.tsx
deleted file mode 100644
index 429b529..0000000
--- a/Frontend/src/components/chat/messages-list.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import { Message } from "@/redux/chatSlice";
-import React, { JSX, useEffect } from "react";
-import { format, isEqual, parseISO } from "date-fns";
-import MessageItem from "./message-item";
-import { useChat } from "@/lib/useChat";
-import { useSelector } from "react-redux";
-import { RootState } from "@/redux/store";
-
-export default function MessagesList({ messages }: { messages: Message[] }) {
- const [lastMarkedAsSeen, setLastMarkedAsSeen] = React.useState(
- new Date().toISOString()
- );
- const selectedChatId = useSelector(
- (state: RootState) => state.chat.selectedChatId
- );
-
- useEffect(() => {
- setLastMarkedAsSeen(new Date().toISOString());
- }, [selectedChatId]);
-
- const { markMessageAsSeen } = useChat();
-
- useEffect(() => {
- const unseenMessages = messages.filter(
- (message) =>
- message.isSent === false &&
- new Date(message.createdAt).getTime() >
- new Date(lastMarkedAsSeen).getTime()
- );
- if (unseenMessages.length > 0) {
- unseenMessages.forEach((message) => {
- markMessageAsSeen(message.chatListId, message.id);
- });
- setLastMarkedAsSeen(new Date().toISOString());
- }
- }, [messages]);
-
- return (
- <>
- {messages.length > 0 ? (
- <>
- {messages.reduce((acc: JSX.Element[], message, index, array) => {
- // Add date separator for first message
- if (index === 0) {
- const firstDate = parseISO(message.createdAt);
- acc.push(
-
-
- {format(firstDate, "PPP")}
-
-
- );
- }
-
- // Add the message component
- acc.push( );
-
- // Check if the next message is from a different date
- if (index < array.length - 1) {
- const currentDate = parseISO(message.createdAt);
- const nextDate = parseISO(array[index + 1].createdAt);
-
- // Check if dates are different
- if (
- !isEqual(
- new Date(
- currentDate.getFullYear(),
- currentDate.getMonth(),
- currentDate.getDate()
- ),
- new Date(
- nextDate.getFullYear(),
- nextDate.getMonth(),
- nextDate.getDate()
- )
- )
- ) {
- acc.push(
-
-
- {format(nextDate, "PPP")}
-
-
- );
- }
- }
-
- return acc;
- }, [])}
- >
- ) : (
-
- )}
- >
- );
-}
diff --git a/Frontend/src/components/chat/messages-view.tsx b/Frontend/src/components/chat/messages-view.tsx
deleted file mode 100644
index 13167e1..0000000
--- a/Frontend/src/components/chat/messages-view.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import React, { useEffect, useRef, useState } from "react";
-import { useDispatch, useSelector } from "react-redux";
-import { RootState } from "@/redux/store";
-import SelectedUserCard from "./selected-user-card";
-import MessageInput from "./message-input";
-import MessagesList from "./messages-list";
-import { useChat } from "@/lib/useChat";
-import { Loader2 } from "lucide-react";
-
-export default function MessagesView() {
- const selectedChatId = useSelector(
- (state: RootState) => state.chat.selectedChatId
- );
- const dispatch = useDispatch();
- const [lastFetchedTime, setLastFetchedTime] = useState(
- new Date().getTime()
- );
- const [loading, setLoading] = useState(false);
- const [hasMore, setHasMore] = useState(true);
- const messagesContainerRef = useRef(null);
- const initialLoadPerformedRef = useRef(false);
-
- const chatMessageIds = useSelector((state: RootState) =>
- selectedChatId ? state.chat.chats[selectedChatId]?.messageIds || [] : []
- );
-
- const { fetchChatMessages, markChatAsSeen } = useChat();
-
- const messages = useSelector((state: RootState) =>
- chatMessageIds.map((messageId) => state.chat.messages[messageId])
- );
-
- // Load messages function (used for both initial and scroll-based loading)
- const loadMessages = (timestamp: number = 0) => {
- if (!selectedChatId || loading || (!hasMore && timestamp !== 0)) return;
-
- setLoading(true);
-
- fetchChatMessages(selectedChatId, timestamp)
- .then((fetchedMessages) => {
- // If no messages or empty array, we've reached the end
- if (!fetchedMessages) {
- setHasMore(false);
- }
-
- // Mark messages as seen on initial load
- if (timestamp === 0) {
- markChatAsSeen(selectedChatId);
- // Scroll to bottom after loading old messages
- setTimeout(() => {
- if (messagesContainerRef.current) {
- messagesContainerRef.current.scrollTop =
- messagesContainerRef.current.scrollHeight;
- }
- }, 1000);
- }
- })
- .finally(() => {
- setLoading(false);
- });
- };
-
- // Reset state when chat changes
- useEffect(() => {
- if (selectedChatId) {
- setHasMore(true);
- setLastFetchedTime(new Date().getTime());
- initialLoadPerformedRef.current = false;
- setTimeout(() => {
- if (messagesContainerRef.current) {
- messagesContainerRef.current.scrollTop =
- messagesContainerRef.current.scrollHeight;
- }
- }, 10);
- }
- }, [selectedChatId]);
-
- // Perform initial load if needed
- useEffect(() => {
- if (
- selectedChatId &&
- chatMessageIds.length === 0 &&
- !initialLoadPerformedRef.current
- ) {
- initialLoadPerformedRef.current = true;
- loadMessages(0);
- }
- }, [selectedChatId, chatMessageIds.length]);
-
- // Update lastFetchedTime when messages change
- useEffect(() => {
- if (messages.length > 0) {
- const oldestMessageTime = new Date(messages[0].createdAt).getTime();
- // Only update if we have a new oldest message
- if (oldestMessageTime < lastFetchedTime) {
- setLastFetchedTime(oldestMessageTime);
- }
- }
- }, [messages, lastFetchedTime]);
-
- // Handle scroll to load more messages
- const handleScroll = () => {
- if (!messagesContainerRef.current) return;
-
- const { scrollTop } = messagesContainerRef.current;
-
- // Check if scrolled to top (with a small threshold)
- if (scrollTop < 10 && !loading && hasMore && selectedChatId) {
- loadMessages(lastFetchedTime);
- }
- };
-
- // Maintain scroll position when adding old messages
- useEffect(() => {
- if (loading && messagesContainerRef.current) {
- const container = messagesContainerRef.current;
- const oldScrollHeight = container.scrollHeight;
-
- setTimeout(() => {
- const newScrollHeight = container.scrollHeight;
- container.scrollTop = newScrollHeight - oldScrollHeight;
- }, 0);
- }
- }, [messages, loading]);
-
- if (!selectedChatId) {
- return (
-
-
Select a chat to view messages
-
- );
- }
-
- return (
-
-
-
-
-
-
- {loading && (
-
-
-
- )}
- {!hasMore && messages.length > 0 && (
-
- No more messages
-
- )}
-
-
-
-
-
- );
-}
diff --git a/Frontend/src/components/chat/selected-user-card.tsx b/Frontend/src/components/chat/selected-user-card.tsx
deleted file mode 100644
index 7c283ad..0000000
--- a/Frontend/src/components/chat/selected-user-card.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import React, { use, useEffect } from "react";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
-import { useDispatch, useSelector } from "react-redux";
-import { RootState } from "@/redux/store";
-import { API_URL } from "@/lib/utils";
-import axios from "axios";
-import {
- markChatAsDelivered,
- setSelectedChat,
- updateReceiverStatus,
-} from "@/redux/chatSlice";
-import { formatDistanceToNow } from "date-fns";
-import { Button } from "../ui/button";
-
-interface UserStatusResponse {
- isOnline: boolean;
- lastSeen?: string;
-}
-
-function formatLastSeen(lastSeen: string | null) {
- if (!lastSeen) return "";
- const date = new Date(lastSeen);
- return `Last seen: ${formatDistanceToNow(date, { addSuffix: true })}`;
-}
-
-export default function SelectedUserCard() {
- const selectedChatId = useSelector(
- (state: RootState) => state.chat.selectedChatId
- );
- const receiver = useSelector(
- (state: RootState) => state.chat.chats[state.chat.selectedChatId!].receiver
- );
- const dispatch = useDispatch();
-
- useEffect(() => {
- if (!selectedChatId) return;
-
- const fetchUserStatus = async () => {
- try {
- const response = await axios.get(
- `${API_URL}/chat/user_status/${receiver.id}`
- );
- dispatch(
- updateReceiverStatus({
- chatListId: selectedChatId,
- isOnline: response.data.isOnline,
- lastSeen: response.data.lastSeen,
- })
- );
- } catch (error) {
- console.error("Error fetching user status:", error);
- }
- };
- const interval = setInterval(() => {
- fetchUserStatus();
- }, 20000);
- fetchUserStatus();
- return () => {
- clearInterval(interval);
- };
- }, [selectedChatId, dispatch, receiver.id]);
-
- useEffect(() => {
- if (receiver.isOnline && selectedChatId) {
- dispatch(
- markChatAsDelivered({
- chatListId: selectedChatId,
- })
- );
- }
- }, [receiver.isOnline, selectedChatId]);
-
- return (
-
-
-
- {receiver.username!.charAt(0)}
-
-
-
{receiver.username}
-
- {receiver.isOnline ? "Online" : formatLastSeen(receiver.lastSeen)}
-
-
-
{
- dispatch(setSelectedChat(null));
- }}
- >
- X
-
-
- );
-}
diff --git a/Frontend/src/components/collaboration-hub/ActiveCollabCard.tsx b/Frontend/src/components/collaboration-hub/ActiveCollabCard.tsx
deleted file mode 100644
index 8ee2872..0000000
--- a/Frontend/src/components/collaboration-hub/ActiveCollabCard.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import React from "react";
-import { useNavigate } from "react-router-dom";
-import { Button } from "../ui/button";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
-
-export interface ActiveCollabCardProps {
- id: number;
- collaborator: {
- name: string;
- avatar: string;
- contentType: string;
- };
- collabTitle: string;
- status: string;
- startDate: string;
- dueDate: string;
- messages: number;
- deliverables: { completed: number; total: number };
- lastActivity: string;
- latestUpdate: string;
-}
-
-const statusColors: Record = {
- "In Progress": "bg-blue-100 text-blue-700",
- "Awaiting Response": "bg-yellow-100 text-yellow-700",
- "Completed": "bg-green-100 text-green-700"
-};
-
-function getDaysBetween(start: string, end: string) {
- const s = new Date(start);
- const e = new Date(end);
- if (isNaN(s.getTime()) || isNaN(e.getTime())) return 0;
- const diff = e.getTime() - s.getTime();
- if (diff < 0) return 0;
- return Math.ceil(diff / (1000 * 60 * 60 * 24));
-}
-
-function getDaysLeft(due: string) {
- const now = new Date();
- const d = new Date(due);
- if (isNaN(d.getTime())) return 0;
- const diff = d.getTime() - now.getTime();
- // Allow negative for overdue, but if invalid, return 0
- return Math.ceil(diff / (1000 * 60 * 60 * 24));
-}
-
-function getTimelineProgress(start: string, due: string) {
- const total = getDaysBetween(start, due);
- if (total === 0) return 0;
- const elapsed = getDaysBetween(start, new Date().toISOString().slice(0, 10));
- return Math.min(100, Math.max(0, Math.round((elapsed / total) * 100)));
-}
-
-const ActiveCollabCard: React.FC = ({
- id,
- collaborator,
- collabTitle,
- status,
- startDate,
- dueDate,
- messages,
- deliverables,
- lastActivity,
- latestUpdate
-}) => {
- const navigate = useNavigate();
- const deliverableProgress = Math.round((deliverables.completed / deliverables.total) * 100);
- const timelineProgress = getTimelineProgress(startDate, dueDate);
- const daysLeft = getDaysLeft(dueDate);
- const overdue = daysLeft < 0 && status !== "Completed";
-
- return (
-
-
-
-
- {collaborator.name.slice(0,2).toUpperCase()}
-
-
-
{collaborator.name}
-
{collaborator.contentType}
-
-
{status}
-
-
- Collab: {collabTitle}
- Start: {startDate}
- Due: {dueDate}
- {overdue ? `Overdue by ${Math.abs(daysLeft)} days` : daysLeft === 0 ? "Due today" : `${daysLeft} days left`}
-
- {/* Timeline Progress Bar */}
-
-
- Timeline
- {timelineProgress}%
-
-
-
- {/* Deliverables Progress Bar */}
-
-
- Deliverables
- {deliverables.completed}/{deliverables.total} ({deliverableProgress}%)
-
-
-
-
- Messages: {messages}
- Last activity: {lastActivity}
-
-
- Latest update: {latestUpdate}
-
-
- navigate(`/dashboard/collaborations/${id}`)}
- aria-label="View collaboration details"
- >
- View Details
-
-
- Message
-
- {status !== "Completed" && (
-
- Mark Complete
-
- )}
-
-
- );
-};
-
-export default ActiveCollabCard;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/ActiveCollabsGrid.tsx b/Frontend/src/components/collaboration-hub/ActiveCollabsGrid.tsx
deleted file mode 100644
index bbad3e4..0000000
--- a/Frontend/src/components/collaboration-hub/ActiveCollabsGrid.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React, { useState } from "react";
-import { activeCollabsMock } from "./activeCollabsMockData";
-import ActiveCollabCard from "./ActiveCollabCard";
-
-const statusOptions = ["All", "In Progress", "Completed"];
-const sortOptions = ["Start Date", "Due Date", "Name"];
-
-const ActiveCollabsGrid: React.FC = () => {
- const [statusFilter, setStatusFilter] = useState("All");
- const [sortBy, setSortBy] = useState("Start Date");
-
- // Only show In Progress and Completed
- let filtered = activeCollabsMock.filter(c => c.status !== "Awaiting Response");
- if (statusFilter !== "All") {
- filtered = filtered.filter(c => c.status === statusFilter);
- }
- if (sortBy === "Start Date") {
- filtered = [...filtered].sort((a, b) => a.startDate.localeCompare(b.startDate));
- } else if (sortBy === "Due Date") {
- filtered = [...filtered].sort((a, b) => a.dueDate.localeCompare(b.dueDate));
- } else if (sortBy === "Name") {
- filtered = [...filtered].sort((a, b) => a.collaborator.name.localeCompare(b.collaborator.name));
- }
-
- return (
-
-
-
- Status:
- setStatusFilter(e.target.value)}
- aria-label="Filter collaborations by status"
- >
- {statusOptions.map(opt => (
- {opt}
- ))}
-
-
-
- Sort by:
- setSortBy(e.target.value)}
- aria-label="Sort collaborations by criteria"
- >
- {sortOptions.map(opt => (
- {opt}
- ))}
-
-
-
- {filtered.length === 0 ? (
-
-
No active collaborations
-
Start a new collaboration to see it here!
-
- ) : (
-
- {filtered.map(collab => (
-
- ))}
-
- )}
-
- );
-};
-
-export default ActiveCollabsGrid;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollabRequests.tsx b/Frontend/src/components/collaboration-hub/CollabRequests.tsx
deleted file mode 100644
index 57cfb2c..0000000
--- a/Frontend/src/components/collaboration-hub/CollabRequests.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import React, { useState } from "react";
-import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "../ui/card";
-import { Button } from "../ui/button";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
-import { Badge } from "../ui/badge";
-import { Separator } from "../ui/separator";
-import { MessageSquare, CheckCircle, XCircle, Lightbulb, TrendingUp, Users, Star, Mail } from "lucide-react";
-
-// Mock data for incoming requests
-const mockRequests = [
- {
- id: 1,
- sender: {
- name: "TechSavvy",
- avatar: "https://randomuser.me/api/portraits/men/32.jpg",
- contentNiche: "Tech Reviews",
- audienceSize: "250K",
- followers: 250000,
- engagement: "4.2%",
- rating: 4.7,
- },
- summary: "Collaboration for a new smartphone launch campaign.",
- proposal: {
- contentLength: "5-7 min video",
- paymentSchedule: "50% upfront, 50% after delivery",
- numberOfPosts: "2 Instagram posts, 1 YouTube video",
- timeline: "Within 3 weeks of product launch",
- notes: "Open to creative input and additional deliverables."
- },
- stats: {
- posts: 120,
- completionRate: "98%",
- avgViews: "80K"
- },
- ai: {
- advantages: [
- "Access to a highly engaged tech audience.",
- "Boosts brand credibility through trusted reviews.",
- "Potential for long-term partnership."
- ],
- ideas: [
- "Unboxing and first impressions video.",
- "Live Q&A session with audience.",
- "Social media giveaway collaboration."
- ],
- recommendations: [
- "Highlight unique features in the first 60 seconds.",
- "Leverage Instagram Stories for behind-the-scenes content.",
- "Schedule a follow-up review after 1 month."
- ]
- }
- },
- {
- id: 2,
- sender: {
- name: "EcoChic",
- avatar: "https://randomuser.me/api/portraits/women/44.jpg",
- contentNiche: "Sustainable Fashion",
- audienceSize: "180K",
- followers: 180000,
- engagement: "5.1%",
- rating: 4.9,
- },
- summary: "Proposal for a sustainable clothing line promotion.",
- proposal: {
- contentLength: "3-5 min Instagram Reel",
- paymentSchedule: "Full payment after campaign",
- numberOfPosts: "1 Reel, 2 Stories",
- timeline: "Next month",
- notes: "Would love to brainstorm eco-friendly angles together."
- },
- stats: {
- posts: 95,
- completionRate: "96%",
- avgViews: "60K"
- },
- ai: {
- advantages: [
- "Tap into eco-conscious audience.",
- "Enhance brand's sustainability image.",
- "Opportunity for co-branded content."
- ],
- ideas: [
- "Instagram Reels styling challenge.",
- "Joint blog post on sustainable fashion tips.",
- "Giveaway of eco-friendly products."
- ],
- recommendations: [
- "Feature behind-the-scenes of production.",
- "Encourage user-generated content with a hashtag.",
- "Host a live styling session."
- ]
- }
- }
-];
-
-const CollabRequests: React.FC = () => {
- const [requests, setRequests] = useState(mockRequests);
-
- const handleAccept = (id: number) => {
- setRequests(prev => prev.filter(req => req.id !== id));
- // TODO: Integrate with backend to accept request
- };
-
- const handleDeny = (id: number) => {
- setRequests(prev => prev.filter(req => req.id !== id));
- // TODO: Integrate with backend to deny request
- };
-
- const handleMessage = (id: number) => {
- // TODO: Open message modal or redirect to chat
- alert("Open chat with sender (not implemented)");
- };
-
- return (
-
- {requests.length === 0 ? (
-
No collaboration requests at this time.
- ) : (
- requests.map((req) => (
-
-
-
-
-
- {req.sender.name.slice(0,2).toUpperCase()}
-
-
-
{req.sender.name}
-
{req.sender.contentNiche} • {req.sender.audienceSize} audience
-
- {req.sender.followers.toLocaleString()} followers
- {req.sender.engagement} engagement
- {req.sender.rating}/5
-
-
-
-
-
-
- Request: {req.summary}
-
- {/* Initial Collaboration Proposal Section */}
-
-
- 📝 Initial Collaboration Proposal
-
-
- Content Length: {req.proposal.contentLength}
- Payment Schedule: {req.proposal.paymentSchedule}
- Number of Posts: {req.proposal.numberOfPosts}
- Timeline: {req.proposal.timeline}
- Notes: {req.proposal.notes}
-
-
-
- {/* AI Advantages */}
-
-
Advantages
-
- {req.ai.advantages.map((adv, i) => {adv} )}
-
-
- {/* AI Ideas */}
-
-
Collaboration Ideas
-
- {req.ai.ideas.map((idea, i) => {idea} )}
-
-
- {/* AI Recommendations */}
-
-
Recommendations
-
- {req.ai.recommendations.map((rec, i) => {rec} )}
-
-
-
-
-
- Posts: {req.stats.posts}
- Completion Rate: {req.stats.completionRate}
- Avg Views: {req.stats.avgViews}
-
-
- handleAccept(req.id)} aria-label="Accept collaboration request">
- Accept
-
- handleDeny(req.id)} aria-label="Deny collaboration request">
- Deny
-
- handleMessage(req.id)} aria-label="Message sender">
- Message
-
-
- Email
-
-
-
-
- ))
- )}
-
- );
-};
-
-export default CollabRequests;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollaborationDeliverables.tsx b/Frontend/src/components/collaboration-hub/CollaborationDeliverables.tsx
deleted file mode 100644
index c2e2291..0000000
--- a/Frontend/src/components/collaboration-hub/CollaborationDeliverables.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from "react";
-import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
-import { Button } from "../ui/button";
-import { Badge } from "../ui/badge";
-import { FileText, Download, Eye, Edit } from "lucide-react";
-
-const CollaborationDeliverables = ({
- mockDeliverables,
- getDeliverableStatusColor,
- handleViewDeliverable,
-}) => (
-
-
-
-
- Deliverables
-
-
-
-
- {mockDeliverables.map((deliverable) => (
-
-
-
-
{deliverable.title}
-
{deliverable.description}
-
-
- {deliverable.status.replace('-', ' ')}
-
-
-
-
- Due Date:
- {deliverable.dueDate}
-
-
- Assigned To:
- {deliverable.assignedTo}
-
-
- {deliverable.files && deliverable.files.length > 0 && (
-
-
Files:
-
- {deliverable.files.map((file, index) => (
-
-
- {file}
-
- ))}
-
-
- )}
-
- handleViewDeliverable(deliverable)}
- >
-
- View
-
-
-
- Edit
-
-
-
- ))}
-
-
-
-);
-
-export default CollaborationDeliverables;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollaborationMessages.tsx b/Frontend/src/components/collaboration-hub/CollaborationMessages.tsx
deleted file mode 100644
index 27633f3..0000000
--- a/Frontend/src/components/collaboration-hub/CollaborationMessages.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from "react";
-import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
-import { Button } from "../ui/button";
-import { Separator } from "../ui/separator";
-import { MessageSquare, Send } from "lucide-react";
-import { Input } from "../ui/input";
-import { Textarea } from "../ui/textarea";
-
-const CollaborationMessages = ({
- mockMessages,
- messageStyles,
- messageStyle,
- showStyleOptions,
- setShowStyleOptions,
- newMessage,
- setNewMessage,
- handleSendMessage,
- handleStyleChange,
- customStyle,
- setCustomStyle,
- handleCustomStyle,
-}) => (
-
-
-
-
- Messages
-
-
-
-
- {mockMessages.map((message) => (
-
-
-
{message.sender}
-
{message.content}
-
{message.timestamp}
-
-
- ))}
-
-
- {/* AI Message Style Enhancement */}
-
-
-
Message Style
- setShowStyleOptions(!showStyleOptions)}
- className="text-xs"
- >
- {messageStyles.find(s => s.value === messageStyle)?.label || "Professional"}
-
-
- {showStyleOptions && (
-
-
- {messageStyles.map((style) => (
- handleStyleChange(style.value)}
- className="text-xs justify-start"
- >
- {style.label}
-
- ))}
-
-
- setCustomStyle(e.target.value)}
- className="flex-1 text-xs"
- onKeyPress={(e) => e.key === 'Enter' && handleCustomStyle()}
- />
-
- Apply
-
-
-
- )}
-
-
- setNewMessage(e.target.value)}
- className="flex-1"
- rows={2}
- />
-
-
-
-
-
-
-);
-
-export default CollaborationMessages;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollaborationMessagesTab.tsx b/Frontend/src/components/collaboration-hub/CollaborationMessagesTab.tsx
deleted file mode 100644
index 9258e7f..0000000
--- a/Frontend/src/components/collaboration-hub/CollaborationMessagesTab.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import React from "react";
-import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
-import { Button } from "../ui/button";
-import { Separator } from "../ui/separator";
-import { Textarea } from "../ui/textarea";
-import { Input } from "../ui/input";
-import { MessageSquare, Send } from "lucide-react";
-
-export default function CollaborationMessagesTab({
- mockMessages,
- messageStyles,
- messageStyle,
- showStyleOptions,
- newMessage,
- setNewMessage,
- handleSendMessage,
- setShowStyleOptions,
- handleStyleChange,
- customStyle,
- setCustomStyle,
- handleCustomStyle
-}: {
- mockMessages: any[];
- messageStyles: any[];
- messageStyle: string;
- showStyleOptions: boolean;
- newMessage: string;
- setNewMessage: (v: string) => void;
- handleSendMessage: () => void;
- setShowStyleOptions: (v: boolean) => void;
- handleStyleChange: (v: string) => void;
- customStyle: string;
- setCustomStyle: (v: string) => void;
- handleCustomStyle: () => void;
-}) {
- return (
-
-
-
-
- Messages
-
-
-
-
- {mockMessages.map((message) => (
-
-
-
{message.sender}
-
{message.content}
-
{message.timestamp}
-
-
- ))}
-
-
- {/* AI Message Style Enhancement */}
-
-
-
Message Style
- setShowStyleOptions(!showStyleOptions)}
- className="text-xs"
- >
- {messageStyles.find(s => s.value === messageStyle)?.label || "Professional"}
-
-
- {showStyleOptions && (
-
-
- {messageStyles.map((style) => (
- handleStyleChange(style.value)}
- className="text-xs justify-start"
- >
- {style.label}
-
- ))}
-
-
- setCustomStyle(e.target.value)}
- className="flex-1 text-xs"
- onKeyPress={e => e.key === 'Enter' && handleCustomStyle()}
- />
- Apply
-
-
- )}
-
-
- setNewMessage(e.target.value)}
- className="flex-1"
- rows={2}
- />
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollaborationOverview.tsx b/Frontend/src/components/collaboration-hub/CollaborationOverview.tsx
deleted file mode 100644
index 9ce5cb0..0000000
--- a/Frontend/src/components/collaboration-hub/CollaborationOverview.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import React from "react";
-import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
-import { Button } from "../ui/button";
-import { Separator } from "../ui/separator";
-import { BarChart3, Users, Edit } from "lucide-react";
-import { Textarea } from "../ui/textarea";
-
-const CollaborationOverview = ({
- collaboration,
- isEditingUpdate,
- editedUpdate,
- handleEditUpdate,
- handleSaveUpdate,
- handleCancelEdit,
- setEditedUpdate,
- getStatusColor,
- getDeliverableStatusColor,
-}) => (
- <>
- {/* Collaboration Summary */}
-
-
-
-
- Collaboration Summary
-
-
-
-
-
-
Project Details
-
-
- Start Date:
- {collaboration.startDate}
-
-
- Due Date:
- {collaboration.dueDate}
-
-
- Content Type:
- {collaboration.collaborator.contentType}
-
-
-
-
-
Progress
-
-
- Deliverables:
- {collaboration.deliverables.completed}/{collaboration.deliverables.total}
-
-
- Messages:
- {collaboration.messages}
-
-
- Last Activity:
- {collaboration.lastActivity}
-
-
-
-
-
-
-
-
Latest Update
- {!isEditingUpdate && (
-
-
- Edit
-
- )}
-
- {isEditingUpdate ? (
-
-
setEditedUpdate(e.target.value)}
- className="min-h-[80px]"
- placeholder="Enter the latest update..."
- />
-
-
- Save
-
-
- Cancel
-
-
-
- ) : (
-
- {collaboration.latestUpdate}
-
- )}
-
-
-
- {/* Progress Tracking */}
-
-
-
-
- Progress Tracking
-
-
-
-
-
- Timeline Progress
- 65%
-
-
-
-
-
- Deliverables Progress
- {Math.round((collaboration.deliverables.completed / collaboration.deliverables.total) * 100)}%
-
-
-
-
-
- {/* AI Project Overview & Recommendations */}
-
-
-
-
- AI Project Overview & Recommendations
-
-
-
-
-
Project Health Analysis
-
-
- This collaboration is progressing well with 65% timeline completion.
- The content creation phase is active and on track.
- Communication frequency is optimal for this stage of the project.
-
-
-
-
-
Current Timeline Recommendations
-
-
-
-
- Content Creation Phase: Consider scheduling a review meeting
- within the next 2 days to ensure alignment on video direction and style.
-
-
-
-
-
- Quality Check: Request a preview of the thumbnail design
- to provide early feedback and avoid last-minute revisions.
-
-
-
-
-
- Risk Mitigation: Prepare backup content ideas in case
- the current direction needs adjustment.
-
-
-
-
-
-
Communication Tips
-
-
- Pro Tip: Use specific feedback when reviewing content.
- Instead of "make it better," try "increase the energy in the first 30 seconds"
- or "add more close-up shots of the product features."
-
-
-
-
-
- >
-);
-
-export default CollaborationOverview;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollaborationOverviewTab.tsx b/Frontend/src/components/collaboration-hub/CollaborationOverviewTab.tsx
deleted file mode 100644
index 5f3b35e..0000000
--- a/Frontend/src/components/collaboration-hub/CollaborationOverviewTab.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import React from "react";
-import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
-import { Button } from "../ui/button";
-import { Separator } from "../ui/separator";
-import { Textarea } from "../ui/textarea";
-import { Edit, BarChart3, Users } from "lucide-react";
-
-export default function CollaborationOverviewTab({
- collaboration,
- isEditingUpdate,
- editedUpdate,
- handleEditUpdate,
- handleSaveUpdate,
- handleCancelEdit,
- setEditedUpdate
-}: {
- collaboration: any;
- isEditingUpdate: boolean;
- editedUpdate: string;
- handleEditUpdate: () => void;
- handleSaveUpdate: () => void;
- handleCancelEdit: () => void;
- setEditedUpdate: (v: string) => void;
-}) {
- return (
- <>
- {/* Collaboration Summary */}
-
-
-
-
- Collaboration Summary
-
-
-
-
-
-
Project Details
-
-
- Start Date:
- {collaboration.startDate}
-
-
- Due Date:
- {collaboration.dueDate}
-
-
- Content Type:
- {collaboration.collaborator.contentType}
-
-
-
-
-
Progress
-
-
- Deliverables:
- {collaboration.deliverables.completed}/{collaboration.deliverables.total}
-
-
- Messages:
- {collaboration.messages}
-
-
- Last Activity:
- {collaboration.lastActivity}
-
-
-
-
-
-
-
-
Latest Update
- {!isEditingUpdate && (
-
-
- Edit
-
- )}
-
- {isEditingUpdate ? (
-
-
setEditedUpdate(e.target.value)}
- className="min-h-[80px]"
- placeholder="Enter the latest update..."
- />
-
- Save
- Cancel
-
-
- ) : (
-
{collaboration.latestUpdate}
- )}
-
-
-
- {/* Progress Tracking */}
-
-
-
-
- Progress Tracking
-
-
-
-
-
- Timeline Progress
- 65%
-
-
-
-
-
- Deliverables Progress
- {Math.round((collaboration.deliverables.completed / collaboration.deliverables.total) * 100)}%
-
-
-
-
-
- >
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollaborationProjectStats.tsx b/Frontend/src/components/collaboration-hub/CollaborationProjectStats.tsx
deleted file mode 100644
index 8906941..0000000
--- a/Frontend/src/components/collaboration-hub/CollaborationProjectStats.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from "react";
-import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
-import { Separator } from "../ui/separator";
-
-export default function CollaborationProjectStats({ collaboration }: { collaboration: any }) {
- return (
-
-
- Project Stats
-
-
-
-
-
-
- {Math.round((collaboration.deliverables.completed / collaboration.deliverables.total) * 100)}%
-
-
Deliverables
-
-
-
-
-
- Days Remaining:
- 3 days
-
-
- Files Shared:
- 5
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollaborationQuickActions.tsx b/Frontend/src/components/collaboration-hub/CollaborationQuickActions.tsx
deleted file mode 100644
index a0b9f85..0000000
--- a/Frontend/src/components/collaboration-hub/CollaborationQuickActions.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from "react";
-import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
-import { Button } from "../ui/button";
-import { Edit, FileText, Download, ExternalLink } from "lucide-react";
-
-export default function CollaborationQuickActions({ handleViewContract }: { handleViewContract: () => void }) {
- return (
-
-
- Quick Actions
-
-
-
-
- Edit Collaboration
-
-
-
- View Contract
-
-
-
- Export Details
-
-
-
- View Profile
-
-
-
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollaborationTimelineTab.tsx b/Frontend/src/components/collaboration-hub/CollaborationTimelineTab.tsx
deleted file mode 100644
index e970aaf..0000000
--- a/Frontend/src/components/collaboration-hub/CollaborationTimelineTab.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import React from "react";
-import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
-import { Button } from "../ui/button";
-import { Calendar, FileText, Eye, Badge } from "lucide-react";
-
-export default function CollaborationTimelineTab({
- mockMilestones,
- handleViewContract,
- contractUrl,
- collaboration,
- getMilestoneStatusColor
-}: {
- mockMilestones: any[];
- handleViewContract: () => void;
- contractUrl: string;
- collaboration: any;
- getMilestoneStatusColor: (status: string) => string;
-}) {
- return (
-
-
-
-
-
- Project Timeline
-
-
-
- View Contract
-
-
-
-
-
- {mockMilestones.map((milestone, index) => (
-
-
-
- {index < mockMilestones.length - 1 && (
-
- )}
-
-
-
-
{milestone.title}
-
- {milestone.status.replace('-', ' ')}
-
-
-
{milestone.description}
-
- Due: {milestone.dueDate}
- {milestone.status === 'in-progress' && (
- {milestone.progress}% complete
- )}
-
- {milestone.status === 'in-progress' && (
-
- )}
-
-
- ))}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CollaboratorSidebar.tsx b/Frontend/src/components/collaboration-hub/CollaboratorSidebar.tsx
deleted file mode 100644
index a6323cc..0000000
--- a/Frontend/src/components/collaboration-hub/CollaboratorSidebar.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from "react";
-import { Card, CardHeader, CardTitle, CardContent } from "../ui/card";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
-import { Separator } from "../ui/separator";
-import { Button } from "../ui/button";
-import { MessageSquare, Mail, Star, TrendingUp, Activity } from "lucide-react";
-
-export default function CollaboratorSidebar({ collaboration }: { collaboration: any }) {
- return (
-
-
- Collaborator
-
-
-
-
-
-
- {collaboration.collaborator.name.slice(0, 2).toUpperCase()}
-
-
-
-
{collaboration.collaborator.name}
-
{collaboration.collaborator.contentType}
-
-
-
-
-
- 4.8/5 rating
-
-
-
- 500K+ followers
-
-
-
-
95% completion rate
-
-
-
-
-
-
- Send Message
-
-
-
- Email
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/ConnectModal.tsx b/Frontend/src/components/collaboration-hub/ConnectModal.tsx
deleted file mode 100644
index 007b30d..0000000
--- a/Frontend/src/components/collaboration-hub/ConnectModal.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import React, { useState } from "react";
-import { mockProfileDetails, mockCollabIdeas, mockRequestTexts, mockWhyMatch } from "./mockProfileData";
-import { Button } from "../ui/button";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
-import { Badge } from "../ui/badge";
-
-interface WhyMatchReason {
- point: string;
- description: string;
-}
-
-interface ConnectModalProps {
- open: boolean;
- onClose: () => void;
- onSend: (selectedText: string) => void;
- matchPercentage?: number;
- whyMatch?: WhyMatchReason[];
-}
-
-const defaultMatch = 98;
-const IDEAS_PER_PAGE = 3;
-
-const ConnectModal: React.FC = ({ open, onClose, onSend, matchPercentage = defaultMatch, whyMatch = mockWhyMatch }) => {
- const [ideasPage, setIdeasPage] = useState(0);
- const [selectedText, setSelectedText] = useState(mockRequestTexts[0]);
- if (!open) return null;
- const profile = mockProfileDetails;
- const totalIdeas = mockCollabIdeas.length;
- if (totalIdeas === 0) {
- return (
-
-
-
×
-
-
No Collaboration Ideas
-
There are currently no AI-generated collaboration ideas available.
-
Close
-
-
-
- );
- }
- const startIdx = (ideasPage * IDEAS_PER_PAGE) % totalIdeas;
- const ideasToShow = Array.from({ length: Math.min(totalIdeas, IDEAS_PER_PAGE) }, (_, i) => mockCollabIdeas[(startIdx + i) % totalIdeas]);
-
- const handleNextIdeas = () => {
- setIdeasPage((prev) => prev + 1);
- };
-
- return (
-
-
-
- ×
-
- {/* Left: Profile Info */}
-
-
- {matchPercentage}% Match
-
-
-
- {profile.name.slice(0,2).toUpperCase()}
-
-
Connect with {profile.name}
-
{profile.contentType}
-
-
Why you match
-
- {whyMatch.map((reason, idx) => (
-
- {reason.point}
- {reason.description}
-
- ))}
-
-
-
- {/* Right: Ideas and Messages */}
-
-
-
AI-Generated Collaboration Ideas
-
- {ideasToShow.length === 0 ? (
- No collaboration ideas available.
- ) : (
- ideasToShow.map((idea, idx) => (
-
- {idea.title}
- {idea.description}
-
- ))
- )}
-
- {totalIdeas > IDEAS_PER_PAGE && (
-
-
- See More Ideas
-
-
- )}
-
-
-
Select a message to send
-
- Select a message to send
- {mockRequestTexts.map((text, idx) => {
- const radioId = `requestText-${idx}`;
- return (
-
- setSelectedText(text)}
- className="mt-1 accent-yellow-400"
- />
-
- {text}
-
-
- );
- })}
-
-
-
- Cancel
- onSend(selectedText)}>Send Request
-
-
-
-
- );
-};
-
-export default ConnectModal;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CreatorMatchCard.tsx b/Frontend/src/components/collaboration-hub/CreatorMatchCard.tsx
deleted file mode 100644
index e50317b..0000000
--- a/Frontend/src/components/collaboration-hub/CreatorMatchCard.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React, { useState } from "react";
-import { Button } from "../ui/button";
-import { Badge } from "../ui/badge";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
-import ViewProfileModal from "./ViewProfileModal";
-import ConnectModal from "./ConnectModal";
-
-export interface CreatorMatchCardProps {
- name: string;
- avatar: string;
- contentType: string;
- matchPercentage: number;
- audienceMatch: string;
- followers: string;
- engagement: string;
- content: string;
- collabs: number;
- whyMatch: string[];
-}
-
-const getAudienceMatchColor = (level: string) => {
- switch (level) {
- case "Very High":
- return "bg-green-500";
- case "High":
- return "bg-yellow-400";
- case "Good":
- return "bg-blue-400";
- default:
- return "bg-gray-300";
- }
-};
-
-// New subcomponents for readability and maintainability
-const CreatorStats: React.FC<{followers: string, engagement: string, content: string, collabs: number}> = ({ followers, engagement, content, collabs }) => (
-
-
Followers {followers}
-
Engagement {engagement}
-
Content {content}
-
Collabs {collabs} completed
-
-);
-
-const AudienceMatchBar: React.FC<{audienceMatch: string}> = ({ audienceMatch }) => (
-
-
Audience Match
-
{audienceMatch}
-
-
-);
-
-const MatchReasons: React.FC<{whyMatch: string[]}> = ({ whyMatch }) => (
-
-
Why you match
-
- {whyMatch.map((reason, idx) => (
- {reason}
- ))}
-
-
-);
-
-export const CreatorMatchCard: React.FC = ({
- name,
- avatar,
- contentType,
- matchPercentage,
- audienceMatch,
- followers,
- engagement,
- content,
- collabs,
- whyMatch,
-}) => {
- const [showProfile, setShowProfile] = useState(false);
- const [showConnect, setShowConnect] = useState(false);
- const [requestSent, setRequestSent] = useState(false);
-
- const handleSendRequest = () => {
- setShowConnect(false);
- setRequestSent(true);
- setTimeout(() => setRequestSent(false), 2000);
- };
-
- return (
- <>
-
-
- {matchPercentage}% Match
-
-
-
-
- {typeof name === 'string' && name.length > 0 ? name.slice(0,2).toUpperCase() : '--'}
-
-
{name}
-
{contentType}
-
-
-
-
-
- setShowProfile(true)}>View Profile
- setShowConnect(true)}>Connect
-
- {requestSent && (
-
Request Sent!
- )}
-
- setShowProfile(false)}
- onConnect={() => {
- setShowProfile(false);
- setTimeout(() => setShowConnect(true), 200);
- }}
- />
- setShowConnect(false)}
- onSend={handleSendRequest}
- />
- >
- );
-};
-
-export default CreatorMatchCard;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx b/Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx
deleted file mode 100644
index 57c1f01..0000000
--- a/Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { useState } from "react";
-import CreatorMatchCard, { CreatorMatchCardProps } from "./CreatorMatchCard";
-
-interface CreatorMatchGridProps {
- creators: CreatorMatchCardProps[];
-}
-
-const PAGE_SIZE = 4;
-
-const CreatorMatchGrid: React.FC = ({ creators }) => {
- const [page, setPage] = useState(0);
- const totalPages = Math.ceil(creators.length / PAGE_SIZE);
-
- const startIdx = page * PAGE_SIZE;
- const endIdx = startIdx + PAGE_SIZE;
- const currentCreators = creators.slice(startIdx, endIdx);
-
- return (
-
-
- {currentCreators.map((creator) => (
-
- ))}
-
-
- setPage((p) => Math.max(p - 1, 0))}
- disabled={page === 0}
- >
- Previous
-
- Page {page + 1} of {totalPages}
- setPage((p) => Math.min(p + 1, totalPages - 1))}
- disabled={page >= totalPages - 1}
- >
- Next
-
-
-
- );
-};
-
-export default CreatorMatchGrid;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/CreatorSearchModal.tsx b/Frontend/src/components/collaboration-hub/CreatorSearchModal.tsx
deleted file mode 100644
index c2623e3..0000000
--- a/Frontend/src/components/collaboration-hub/CreatorSearchModal.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import React, { useState } from "react";
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
-import { Button } from "../ui/button";
-import { Textarea } from "../ui/textarea";
-import { Card, CardContent } from "../ui/card";
-import ViewProfileModal from "./ViewProfileModal";
-import { mockProfileDetails } from "./mockProfileData";
-
-interface CreatorSearchModalProps {
- open: boolean;
- onClose: () => void;
- onConnect?: (creator: any) => void;
-}
-
-export default function CreatorSearchModal({ open, onClose, onConnect }: CreatorSearchModalProps) {
- const [aiSearchDesc, setAiSearchDesc] = useState("");
- const [aiSearchResults, setAiSearchResults] = useState([]);
- const [aiSearchSubmitted, setAiSearchSubmitted] = useState(false);
- const [showProfile, setShowProfile] = useState(false);
-
- // Mock AI search handler
- const handleAiSearch = () => {
- setAiSearchResults([
- mockProfileDetails, // You can add more mock creators if desired
- ]);
- setAiSearchSubmitted(true);
- };
-
- const handleResetAiSearch = () => {
- setAiSearchDesc("");
- setAiSearchResults([]);
- setAiSearchSubmitted(false);
- onClose();
- };
-
- const handleConnect = (creator: any) => {
- if (onConnect) {
- onConnect(creator);
- }
- };
-
- return (
- <>
- { if (!v) handleResetAiSearch(); }}>
-
-
- Find Creators with AI
-
-
- Describe your project or collaboration needs
- ) => setAiSearchDesc(e.target.value)}
- rows={3}
- />
-
-
- Cancel
-
- Find Creators
-
-
-
- {aiSearchSubmitted && (
-
-
Top AI-Suggested Creators
-
- {aiSearchResults.map((creator, idx) => (
-
-
-
-
{creator.name}
-
{creator.contentType} • {creator.location}
-
- setShowProfile(true)}
- >
- View Profile
-
- handleConnect(creator)}
- >
- Connect
-
-
-
- ))}
-
-
- )}
-
-
-
- setShowProfile(false)}
- onConnect={() => {
- setShowProfile(false);
- if (aiSearchResults.length > 0) {
- handleConnect(aiSearchResults[0]);
- }
- }}
- />
- >
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/NewCollaborationModal.tsx b/Frontend/src/components/collaboration-hub/NewCollaborationModal.tsx
deleted file mode 100644
index d3a3585..0000000
--- a/Frontend/src/components/collaboration-hub/NewCollaborationModal.tsx
+++ /dev/null
@@ -1,292 +0,0 @@
-import React, { useState } from "react";
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
-import { Button } from "../ui/button";
-import { Input } from "../ui/input";
-import { Textarea } from "../ui/textarea";
-import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
-import ViewProfileModal from "./ViewProfileModal";
-import { mockProfileDetails } from "./mockProfileData";
-
-interface NewCollaborationModalProps {
- open: boolean;
- onClose: () => void;
- onSubmit?: (data: any) => void;
-}
-
-interface ProposalData {
- contentLength: string;
- paymentSchedule: string;
- numberOfPosts: string;
- timeline: string;
- notes: string;
-}
-
-export default function NewCollaborationModal({ open, onClose, onSubmit }: NewCollaborationModalProps) {
- const [modalStep, setModalStep] = useState(1);
- const [searchTerm, setSearchTerm] = useState("");
- const [selectedCreator, setSelectedCreator] = useState(null);
- const [showProfile, setShowProfile] = useState(false);
- const [collabDesc, setCollabDesc] = useState("");
- const [aiDesc, setAiDesc] = useState("");
- const [proposal, setProposal] = useState({
- contentLength: "",
- paymentSchedule: "",
- numberOfPosts: "",
- timeline: "",
- notes: ""
- });
- const [aiProposal, setAiProposal] = useState(null);
- const [reviewed, setReviewed] = useState(false);
-
- // Mock creator search (returns mockProfileDetails for any search)
- const searchResults = searchTerm ? [mockProfileDetails] : [];
-
- // Mock AI suggestions
- const handleAiDesc = () => {
- setAiDesc("AI Suggestion: Collaborate on a tech review series with cross-promotion and audience Q&A.");
- };
-
- const handleAiProposal = () => {
- setAiProposal({
- contentLength: "5-7 min video",
- paymentSchedule: "50% upfront, 50% after delivery",
- numberOfPosts: "2 Instagram posts, 1 YouTube video",
- timeline: "Within 3 weeks of product launch",
- notes: "Open to creative input and additional deliverables."
- });
- };
-
- const handleResetModal = () => {
- setModalStep(1);
- setSearchTerm("");
- setSelectedCreator(null);
- setShowProfile(false);
- setCollabDesc("");
- setAiDesc("");
- setProposal({ contentLength: "", paymentSchedule: "", numberOfPosts: "", timeline: "", notes: "" });
- setAiProposal(null);
- setReviewed(false);
- onClose();
- };
-
- const handleSubmit = () => {
- setReviewed(true);
- setTimeout(() => {
- if (onSubmit) {
- onSubmit({
- creator: selectedCreator,
- description: collabDesc || aiDesc,
- proposal,
- reviewed: true
- });
- }
- handleResetModal();
- }, 1500);
- };
-
- return (
- <>
- { if (!v) handleResetModal(); }}>
-
-
- New Collaboration Request
-
-
- {/* Stepper */}
-
-
1. Search Creator
-
2. Describe Collab
-
3. Proposal Details
-
4. Review & Send
-
-
- {/* Step 1: Search Creator */}
- {modalStep === 1 && (
-
-
setSearchTerm(e.target.value)}
- className="mb-4"
- />
- {searchResults.length > 0 ? (
-
- {searchResults.map((creator, idx) => (
-
-
-
-
{creator.name}
-
{creator.contentType} • {creator.location}
-
- { setSelectedCreator(creator); setShowProfile(true); }}
- >
- View Profile
-
- setSelectedCreator(creator)}
- >
- Select
-
-
-
- ))}
-
- ) : (
-
Type a name to search for creators.
- )}
-
- Cancel
- setModalStep(2)}
- >
- Next
-
-
-
- )}
-
- {/* Step 2: Describe Collab */}
- {modalStep === 2 && (
-
-
Describe the collaboration you're looking for
-
) => setCollabDesc(e.target.value)}
- rows={3}
- />
-
-
AI Suggest
- {aiDesc && (
-
- {aiDesc}
-
- )}
-
-
- Cancel
- setModalStep(1)}>Back
- setModalStep(3)}
- disabled={!collabDesc && !aiDesc}
- >
- Next
-
-
-
- )}
-
- {/* Step 3: Proposal Details */}
- {modalStep === 3 && (
-
-
Proposal Details
-
- setProposal({ ...proposal, contentLength: e.target.value })}
- />
- setProposal({ ...proposal, paymentSchedule: e.target.value })}
- />
- setProposal({ ...proposal, numberOfPosts: e.target.value })}
- />
- setProposal({ ...proposal, timeline: e.target.value })}
- />
-
-
) => setProposal({ ...proposal, notes: e.target.value })}
- className="mt-2"
- />
-
-
AI Draft Proposal
- {aiProposal && (
-
-
AI Proposal:
-
Content Length: {aiProposal.contentLength}
-
Payment: {aiProposal.paymentSchedule}
-
Posts: {aiProposal.numberOfPosts}
-
Timeline: {aiProposal.timeline}
-
Notes: {aiProposal.notes}
-
setProposal(aiProposal)}
- >
- Use This
-
-
- )}
-
-
- Cancel
- setModalStep(2)}>Back
- setModalStep(4)}
- disabled={!proposal.contentLength || !proposal.paymentSchedule || !proposal.numberOfPosts || !proposal.timeline}
- >
- Next
-
-
-
- )}
-
- {/* Step 4: Review & Send */}
- {modalStep === 4 && (
-
-
Review & Send
-
-
- To: {selectedCreator?.name}
-
-
- Description: {collabDesc || aiDesc}
- Content Length: {proposal.contentLength}
- Payment Schedule: {proposal.paymentSchedule}
- Number of Posts: {proposal.numberOfPosts}
- Timeline: {proposal.timeline}
- Notes: {proposal.notes}
-
-
-
- Cancel
- setModalStep(3)}>Back
-
- Send Request
-
-
- {reviewed && (
-
Request Sent!
- )}
-
- )}
-
-
-
- setShowProfile(false)}
- onConnect={() => { setShowProfile(false); setSelectedCreator(searchResults[0]); }}
- />
- >
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/ViewProfileModal.tsx b/Frontend/src/components/collaboration-hub/ViewProfileModal.tsx
deleted file mode 100644
index 138b242..0000000
--- a/Frontend/src/components/collaboration-hub/ViewProfileModal.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from "react";
-import { mockProfileDetails, mockWhyMatch } from "./mockProfileData";
-import { Button } from "../ui/button";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
-import { Badge } from "../ui/badge";
-
-interface WhyMatchReason {
- point: string;
- description: string;
-}
-
-const defaultMatch = 98;
-
-interface ViewProfileModalProps {
- open: boolean;
- onClose: () => void;
- onConnect: () => void;
- matchPercentage?: number;
- whyMatch?: WhyMatchReason[];
-}
-
-const ViewProfileModal: React.FC = ({ open, onClose, onConnect, matchPercentage = defaultMatch, whyMatch = mockWhyMatch }) => {
- if (!open) return null;
- const profile = mockProfileDetails;
- return (
-
-
-
- ×
-
-
-
- {matchPercentage}% Match
-
-
-
- {profile.name.slice(0,2).toUpperCase()}
-
-
{profile.name}
-
{profile.contentType} • {profile.location}
-
-
{profile.bio}
-
-
-
Followers {profile.followers}
-
Engagement {profile.engagement}
-
Content {profile.content}
-
Collabs {profile.collabs} completed
-
-
-
Why you match
-
- {whyMatch.map((reason, idx) => (
-
- {reason.point}
- {reason.description}
-
- ))}
-
-
-
- Connect
-
-
-
- );
-};
-
-export default ViewProfileModal;
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/activeCollabsMockData.ts b/Frontend/src/components/collaboration-hub/activeCollabsMockData.ts
deleted file mode 100644
index 95d1b06..0000000
--- a/Frontend/src/components/collaboration-hub/activeCollabsMockData.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// MOCK DATA: This will be replaced by functioning backend logic later on
-
-export const activeCollabsMock = [
- {
- id: 1,
- collaborator: {
- name: "GadgetGuru",
- avatar: "/placeholder.svg?height=96&width=96",
- contentType: "Unboxing & First Impressions"
- },
- collabTitle: "Unboxing Marathon",
- status: "In Progress",
- startDate: "2024-06-01",
- dueDate: "2024-06-15",
- messages: 12,
- deliverables: { completed: 2, total: 3 },
- lastActivity: "2 days ago",
- latestUpdate: "Finalizing thumbnail for the main video."
- },
- {
- id: 2,
- collaborator: {
- name: "TechTalker",
- avatar: "/placeholder.svg?height=96&width=96",
- contentType: "Tech News & Commentary"
- },
- collabTitle: "Tech for Good",
- status: "Awaiting Response",
- startDate: "2024-06-05",
- dueDate: "2024-06-20",
- messages: 5,
- deliverables: { completed: 0, total: 2 },
- lastActivity: "5 days ago",
- latestUpdate: "Waiting for approval on the script."
- },
- {
- id: 3,
- collaborator: {
- name: "StyleStar",
- avatar: "/placeholder.svg?height=96&width=96",
- contentType: "Fashion & Lifestyle"
- },
- collabTitle: "Style Swap Challenge",
- status: "Completed",
- startDate: "2024-05-10",
- dueDate: "2024-05-25",
- messages: 18,
- deliverables: { completed: 2, total: 2 },
- lastActivity: "1 day ago",
- latestUpdate: "Collab video published and shared on socials."
- }
-];
\ No newline at end of file
diff --git a/Frontend/src/components/collaboration-hub/mockProfileData.ts b/Frontend/src/components/collaboration-hub/mockProfileData.ts
deleted file mode 100644
index 47212dd..0000000
--- a/Frontend/src/components/collaboration-hub/mockProfileData.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-// MOCK DATA: This will be replaced by functioning backend/AI logic later on
-
-export const mockProfileDetails = {
- id: 1,
- name: "TechReviewer",
- avatar: "/placeholder.svg?height=96&width=96",
- contentType: "Tech Reviews & Tutorials",
- location: "San Francisco, CA",
- bio: "Passionate about the latest in tech. I review gadgets, apps, and everything in between. Always looking for new collab opportunities!",
- followers: "1.2M",
- engagement: "4.8%",
- content: "Tech Reviews",
- collabs: 12,
- socialLinks: [
- { platform: "Instagram", url: "https://instagram.com/techreviewer", icon: "instagram" },
- { platform: "YouTube", url: "https://youtube.com/techreviewer", icon: "youtube" },
- { platform: "Twitter", url: "https://twitter.com/techreviewer", icon: "twitter" }
- ]
-};
-
-export const mockCollabIdeas = [
- {
- title: "Gadget Showdown",
- description: "Compare the latest gadgets in a head-to-head review with unique perspectives from both creators."
- },
- {
- title: "Tech for Good",
- description: "Collaborate on a series highlighting technology that makes a positive impact on society."
- },
- {
- title: "Unboxing Marathon",
- description: "Host a live unboxing event featuring products from both creators' favorite brands."
- },
- {
- title: "Ask the Experts",
- description: "A Q&A session where both creators answer audience tech questions together."
- }
-];
-
-export const mockRequestTexts = [
- "Hi! I love your content. Would you be interested in collaborating on a tech review series?",
- "Hey! I think our audiences would both enjoy a joint unboxing event. Let me know if you're interested!",
- "Hello! I have some ideas for a tech-for-good collaboration. Would you like to connect and discuss?"
-];
-
-export const mockWhyMatch = [
- {
- point: "Complementary content styles",
- description: "Your focus on in-depth reviews complements their quick unboxing and first impressions, offering audiences a full spectrum of tech insights."
- },
- {
- point: "85% audience demographic overlap",
- description: "Both of your audiences are primarily tech enthusiasts aged 18-35, ensuring high engagement and relevance for collaborative content."
- },
- {
- point: "Similar engagement patterns",
- description: "Both channels see peak engagement during product launch weeks, making joint campaigns more impactful."
- }
-];
\ No newline at end of file
diff --git a/Frontend/src/components/contracts/contract-generator.tsx b/Frontend/src/components/contracts/contract-generator.tsx
deleted file mode 100644
index 8fe1b2f..0000000
--- a/Frontend/src/components/contracts/contract-generator.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import { useState } from "react"
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Textarea } from "@/components/ui/textarea"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-import { Slider } from "@/components/ui/slider"
-import { Switch } from "@/components/ui/switch"
-import { Download, FileText, Loader2, Sparkles } from "lucide-react"
-
-export function ContractGenerator() {
- const [isGenerating, setIsGenerating] = useState(false)
- const [isGenerated, setIsGenerated] = useState(false)
-
- const handleGenerate = () => {
- setIsGenerating(true)
- // Simulate AI generation
- setTimeout(() => {
- setIsGenerating(false)
- setIsGenerated(true)
- }, 2000)
- }
-
- return (
-
-
-
- AI Contract Generator
- Generate a customized contract based on your specific needs
-
-
-
- Contract Name
-
-
-
-
- Contract Type
-
-
-
-
-
- Brand Sponsorship
- Creator Collaboration
- Affiliate Partnership
- Content Licensing
-
-
-
-
-
- Other Party Name
-
-
-
-
- Contract Value
-
-
-
-
-
Contract Duration
-
-
-
-
- 1 day
- 30 days
- 180 days
-
-
-
-
- Deliverables
-
-
-
-
-
- Include exclusivity clause
-
-
-
-
- Include revision terms
-
-
-
-
- {isGenerating ? (
- <>
-
- Generating Contract...
- >
- ) : (
- <>
-
- Generate AI Contract
- >
- )}
-
-
-
-
-
-
- Generated Contract
-
- {isGenerated
- ? "Your AI-generated contract is ready for review"
- : "Your contract will appear here after generation"}
-
-
-
- {isGenerated ? (
-
-
-
EcoStyle Sponsorship Agreement
-
-
This Agreement is entered into as of [Date] by and between:
-
- Creator: [Your Name]
-
-
- Brand: EcoStyle
-
-
1. SCOPE OF SERVICES
-
Creator agrees to create and publish the following content:
-
- 1 dedicated Instagram post featuring EcoStyle products
- 2 Instagram stories with swipe-up links
-
-
2. COMPENSATION
-
Brand agrees to pay Creator the sum of $3,000 USD for the Services.
-
Payment Schedule: 50% upon signing, 50% upon completion.
-
3. TERM
-
This Agreement shall commence on the Effective Date and continue for 30 days.
-
[...additional contract sections...]
-
-
-
- ) : (
-
-
-
- Fill out the form and click "Generate AI Contract" to create your customized agreement
-
-
- )}
-
- {isGenerated && (
-
- Edit Contract
-
-
- Download Contract
-
-
- )}
-
-
- )
-}
-
diff --git a/Frontend/src/components/contracts/contract-templates.tsx b/Frontend/src/components/contracts/contract-templates.tsx
deleted file mode 100644
index e760c2f..0000000
--- a/Frontend/src/components/contracts/contract-templates.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import React from "react";
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { FileText, Copy, ArrowRight } from "lucide-react";
-import { useState } from "react";
-import { useNavigate } from "react-router-dom";
-// import { useRouter } from "next/navigation"
-
-const templates = [
- {
- id: "sponsorship",
- title: "Brand Sponsorship",
- description: "Standard agreement for sponsored content with brands",
- features: [
- "Payment terms",
- "Content requirements",
- "Approval process",
- "Usage rights",
- "Exclusivity clauses",
- ],
- },
- {
- id: "collaboration",
- title: "Creator Collaboration",
- description: "Agreement for collaborating with other creators",
- features: [
- "Revenue sharing",
- "Content ownership",
- "Cross-promotion",
- "Creative control",
- "Termination clauses",
- ],
- },
- {
- id: "affiliate",
- title: "Affiliate Partnership",
- description: "Agreement for affiliate marketing relationships",
- features: [
- "Commission structure",
- "Tracking methods",
- "Payment schedule",
- "Promotional guidelines",
- "Term length",
- ],
- },
- {
- id: "licensing",
- title: "Content Licensing",
- description: "License your content to brands or platforms",
- features: [
- "Usage rights",
- "Licensing fees",
- "Term limitations",
- "Attribution requirements",
- "Territorial rights",
- ],
- },
- {
- id: "longterm",
- title: "Long-term Partnership",
- description: "Extended partnership with brands or creators",
- features: [
- "Multi-phase deliverables",
- "Performance metrics",
- "Renewal terms",
- "Escalating compensation",
- "Exit clauses",
- ],
- },
- {
- id: "oneoff",
- title: "One-off Project",
- description: "Simple agreement for a single content piece",
- features: [
- "Single payment",
- "Limited deliverables",
- "Quick turnaround",
- "Minimal obligations",
- "Simple terms",
- ],
- },
-];
-
-export function ContractTemplates() {
- const [selectedTemplate, setSelectedTemplate] = useState(null);
- const navigate = useNavigate();
-
- const handleUseTemplate = (templateId: string) => {
- setSelectedTemplate(templateId);
- // In a real app, this would navigate to a contract creation page with the template pre-loaded
- navigate(`/dashboard/contracts/create?template=${templateId}`);
- };
-
- return (
-
-
- {templates.map((template) => (
-
-
-
-
- {template.title}
-
- {template.description}
-
-
-
- {template.features.map((feature, index) => (
-
-
- {feature}
-
- ))}
-
-
-
- console.log(`Duplicating ${template.id}`)}
- >
-
- Duplicate
-
- handleUseTemplate(template.id)}>
- Use Template
-
-
-
-
- ))}
-
-
- );
-}
diff --git a/Frontend/src/components/dashboard/creator-collaborations.tsx b/Frontend/src/components/dashboard/creator-collaborations.tsx
deleted file mode 100644
index 41832c6..0000000
--- a/Frontend/src/components/dashboard/creator-collaborations.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React from 'react'
-
-// MOCK DATA: This will be replaced by functioning backend logic later on
-export const mockCreatorMatches = [
- {
- id: 1,
- name: "TechReviewer",
- avatar: "https://via.placeholder.com/96",
- contentType: "Tech Reviews & Tutorials",
- matchPercentage: 98,
- audienceMatch: "Very High",
- followers: "1.2M",
- engagement: "4.8%",
- content: "Tech Reviews",
- collabs: 12,
- whyMatch: [
- "Complementary content styles",
- "85% audience demographic overlap",
- "Similar engagement patterns"
- ]
- },
- {
- id: 2,
- name: "GadgetGuru",
- avatar: "https://via.placeholder.com/96",
- contentType: "Unboxing & First Impressions",
- matchPercentage: 92,
- audienceMatch: "High",
- followers: "850K",
- engagement: "5.2%",
- content: "Unboxing",
- collabs: 8,
- whyMatch: [
- "Your reviews + their unboxings = perfect combo",
- "78% audience demographic overlap",
- "Different posting schedules (opportunity)"
- ]
- },
- {
- id: 3,
- name: "TechTalker",
- avatar: "https://via.placeholder.com/96",
- contentType: "Tech News & Commentary",
- matchPercentage: 87,
- audienceMatch: "Good",
- followers: "1.5M",
- engagement: "3.9%",
- content: "Tech News",
- collabs: 15,
- whyMatch: [
- "Their news + your reviews = full coverage",
- "65% audience demographic overlap",
- "Complementary content calendars"
- ]
- }
-];
-
-function CreatorCollaborations() {
- return (
- creator-collaborations
- )
-}
-
-export default CreatorCollaborations
\ No newline at end of file
diff --git a/Frontend/src/components/dashboard/creator-matches.tsx b/Frontend/src/components/dashboard/creator-matches.tsx
deleted file mode 100644
index 2252819..0000000
--- a/Frontend/src/components/dashboard/creator-matches.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { useEffect, useState } from "react";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
-import { Button } from "../ui/button";
-import { Card, CardContent } from "../ui/card";
-import { Badge } from "../ui/badge";
-
-interface CreatorMatch {
- user_id: string;
- match_score: number;
- audience_age_group?: Record;
- audience_location?: Record;
- engagement_rate?: number;
- average_views?: number;
- price_expectation?: number;
- // Add more fields as needed
-}
-
-interface CreatorMatchesProps {
- sponsorshipId: string;
-}
-
-export function CreatorMatches({ sponsorshipId }: CreatorMatchesProps) {
- const [matches, setMatches] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- if (!sponsorshipId) return;
- setLoading(true);
- setError(null);
- fetch(`/api/match/creators-for-brand/${sponsorshipId}`)
- .then((res) => {
- if (!res.ok) throw new Error("Failed to fetch matches");
- return res.json();
- })
- .then((data) => {
- setMatches(data.matches || []);
- setLoading(false);
- })
- .catch((err) => {
- setError(err.message);
- setLoading(false);
- });
- }, [sponsorshipId]);
-
- if (!sponsorshipId) return Select a campaign to see matches.
;
- if (loading) return Loading matches...
;
- if (error) return {error}
;
- if (matches.length === 0) return No matching creators found.
;
-
- return (
-
- {matches.map((creator) => (
-
-
-
-
-
- {creator.user_id.slice(0, 2).toUpperCase()}
-
-
-
-
Creator {creator.user_id.slice(0, 6)}
- {Math.round((creator.match_score / 4) * 100)}% Match
-
-
- Engagement: {creator.engagement_rate ?? "-"}% | Avg Views: {creator.average_views ?? "-"}
-
-
- ${creator.price_expectation ?? "-"}
-
-
- View Profile
-
- Contact
-
-
-
-
-
-
- ))}
-
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/components/dashboard/performance-metrics.tsx b/Frontend/src/components/dashboard/performance-metrics.tsx
deleted file mode 100644
index 057e779..0000000
--- a/Frontend/src/components/dashboard/performance-metrics.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
-
-const data = [
- {
- name: "Jan",
- revenue: 4000,
- engagement: 2400,
- },
- {
- name: "Feb",
- revenue: 3000,
- engagement: 1398,
- },
- {
- name: "Mar",
- revenue: 2000,
- engagement: 9800,
- },
- {
- name: "Apr",
- revenue: 2780,
- engagement: 3908,
- },
- {
- name: "May",
- revenue: 5890,
- engagement: 4800,
- },
- {
- name: "Jun",
- revenue: 4390,
- engagement: 3800,
- },
-]
-
-export function PerformanceMetrics() {
- return (
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
diff --git a/Frontend/src/components/dashboard/recent-activity.tsx b/Frontend/src/components/dashboard/recent-activity.tsx
deleted file mode 100644
index 2f70eb3..0000000
--- a/Frontend/src/components/dashboard/recent-activity.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"
-
-export function RecentActivity() {
- return (
-
-
-
-
- BF
-
-
-
Beauty Fusion
-
Sent you a sponsorship proposal
-
2 hours ago
-
-
-
-
-
- JD
-
-
-
Jane Doe
-
Accepted your collaboration request
-
5 hours ago
-
-
-
-
-
- TS
-
-
-
TechStart
-
Contract signed and finalized
-
Yesterday
-
-
-
-
-
- AI
-
-
-
Inpact AI
-
New sponsorship matches available
-
2 days ago
-
-
-
- )
-}
-
diff --git a/Frontend/src/components/dashboard/sponsorship-matches.tsx b/Frontend/src/components/dashboard/sponsorship-matches.tsx
deleted file mode 100644
index daf6ee8..0000000
--- a/Frontend/src/components/dashboard/sponsorship-matches.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { useEffect, useState } from "react";
-import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"
-import { Button } from "../ui/button"
-import { Card, CardContent } from "../ui/card"
-import { Badge } from "../ui/badge"
-
-interface SponsorshipMatch {
- sponsorship_id: string;
- match_score: number;
- title?: string;
- description?: string;
- budget?: number;
- // Add more fields as needed
-}
-
-interface SponsorshipMatchesProps {
- creatorId: string;
-}
-
-export function SponsorshipMatches({ creatorId }: SponsorshipMatchesProps) {
- const [matches, setMatches] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- if (!creatorId) return;
- setLoading(true);
- setError(null);
- fetch(`/api/match/brands-for-creator/${creatorId}`)
- .then((res) => {
- if (!res.ok) throw new Error("Failed to fetch matches");
- return res.json();
- })
- .then((data) => {
- setMatches(data.matches || []);
- setLoading(false);
- })
- .catch((err) => {
- setError(err.message);
- setLoading(false);
- });
- }, [creatorId]);
-
- if (!creatorId) return Login to see your matches.
;
- if (loading) return Loading matches...
;
- if (error) return {error}
;
- if (matches.length === 0) return No matching brand campaigns found.
;
-
- return (
-
- {matches.map((sponsorship) => (
-
-
-
-
-
- {(sponsorship.title || "BR").slice(0, 2).toUpperCase()}
-
-
-
-
{sponsorship.title || "Brand Campaign"}
- {Math.round((sponsorship.match_score / 4) * 100)}% Match
-
-
- {sponsorship.description || "No description provided."}
-
-
- ${sponsorship.budget ?? "-"}
-
-
- View Details
-
- Contact
-
-
-
-
-
-
- ))}
-
- )
-}
-
diff --git a/Frontend/src/components/date-range-picker.tsx b/Frontend/src/components/date-range-picker.tsx
deleted file mode 100644
index 0eccd2b..0000000
--- a/Frontend/src/components/date-range-picker.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-"use client";
-
-import * as React from "react";
-import { CalendarIcon } from "lucide-react";
-import { format } from "date-fns";
-import type { DateRange } from "react-day-picker";
-
-import { cn } from "@/lib/utils";
-import { Button } from "@/components/ui/button";
-import { Calendar } from "@/components/ui/calendar";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-
-export function DateRangePicker({
- className,
-}: React.HTMLAttributes) {
- const [date, setDate] = React.useState({
- from: new Date(2024, 0, 1),
- to: new Date(),
- });
-
- return (
-
-
-
-
-
- {date?.from ? (
- date.to ? (
- <>
- {format(date.from, "LLL dd, y")} -{" "}
- {format(date.to, "LLL dd, y")}
- >
- ) : (
- format(date.from, "LLL dd, y")
- )
- ) : (
- Pick a date
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/Frontend/src/components/loading.tsx b/Frontend/src/components/loading.tsx
deleted file mode 100644
index d644a79..0000000
--- a/Frontend/src/components/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-export default function Loading() {
- return null
- }
-
-
\ No newline at end of file
diff --git a/Frontend/src/components/main-nav.tsx b/Frontend/src/components/main-nav.tsx
deleted file mode 100644
index 9aa97aa..0000000
--- a/Frontend/src/components/main-nav.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Link } from "react-router-dom"
-
-export function MainNav() {
- return (
-
- {/* Navigation items removed - keeping component for future use */}
- {/* TODO: Under construction - menu items coming soon */}
- Menu coming soon
-
- )
-}
-
diff --git a/Frontend/src/components/mode-toggle.tsx b/Frontend/src/components/mode-toggle.tsx
deleted file mode 100644
index ff4a8c4..0000000
--- a/Frontend/src/components/mode-toggle.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Moon, Sun } from "lucide-react";
-import { useTheme } from "./theme-provider";
-import { Button } from "./ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "./ui/dropdown-menu";
-
-export function ModeToggle() {
- const { setTheme } = useTheme();
-
- return (
-
-
-
-
-
- Toggle theme
-
-
-
- setTheme("light")}>
- Light
-
- setTheme("dark")}>
- Dark
-
- setTheme("system")}>
- System
-
-
-
- );
-}
diff --git a/Frontend/src/components/theme-provider.tsx b/Frontend/src/components/theme-provider.tsx
deleted file mode 100644
index cbcd77d..0000000
--- a/Frontend/src/components/theme-provider.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from "react";
-
-import { createContext, useContext, useEffect, useState } from "react";
-
-const ThemeProviderContext = createContext({
- theme: "system",
- setTheme: (_: string) => null,
-});
-
-export function ThemeProvider({
- children,
- defaultTheme = "system",
- storageKey = "vite-ui-theme",
- ...props
-}: any) {
- const [theme, setTheme] = useState(
- () => localStorage.getItem(storageKey) || defaultTheme
- );
-
- useEffect(() => {
- const root = window.document.documentElement;
-
- root.classList.remove("light", "dark");
-
- if (theme === "system") {
- const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
- .matches
- ? "dark"
- : "light";
-
- root.classList.add(systemTheme);
- return;
- }
-
- root.classList.add(theme);
- }, [theme]);
-
- const value = {
- theme,
- setTheme: (theme: string) => {
- localStorage.setItem(storageKey, theme);
- setTheme(theme);
- },
- };
-
- return (
-
- {children}
-
- );
-}
-
-export const useTheme = () => {
- const context = useContext(ThemeProviderContext);
-
- if (context === undefined)
- throw new Error("useTheme must be used within a ThemeProvider");
-
- return context;
-};
diff --git a/Frontend/src/components/ui/avatar.tsx b/Frontend/src/components/ui/avatar.tsx
deleted file mode 100644
index d260623..0000000
--- a/Frontend/src/components/ui/avatar.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as AvatarPrimitive from "@radix-ui/react-avatar";
-
-import { cn } from "@/lib/utils";
-
-const Avatar = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-Avatar.displayName = AvatarPrimitive.Root.displayName;
-
-const AvatarImage = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AvatarImage.displayName = AvatarPrimitive.Image.displayName;
-
-const AvatarFallback = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
-
-export { Avatar, AvatarImage, AvatarFallback };
diff --git a/Frontend/src/components/ui/badge.tsx b/Frontend/src/components/ui/badge.tsx
deleted file mode 100644
index 9ec9a1a..0000000
--- a/Frontend/src/components/ui/badge.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as React from "react";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-const badgeVariants = cva(
- "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
- destructive:
- "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
- outline: "text-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-);
-
-export interface BadgeProps
- extends React.HTMLAttributes,
- VariantProps {}
-
-function Badge({ className, variant, ...props }: BadgeProps) {
- return (
-
- );
-}
-
-export { Badge, badgeVariants };
diff --git a/Frontend/src/components/ui/button.tsx b/Frontend/src/components/ui/button.tsx
deleted file mode 100644
index edf4421..0000000
--- a/Frontend/src/components/ui/button.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import * as React from "react";
-import { cn } from "../../lib/utils";
-import { Slot } from "@radix-ui/react-slot";
-import { cva, type VariantProps } from "class-variance-authority";
-
-const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
- {
- variants: {
- variant: {
- default: "bg-gray-50 text-gray-600 hover:bg-gray-300",
- destructive: "bg-destructive text-gray-600 hover:bg-gray-50",
- outline:
- "border border-gray-200 bg-white hover:bg-gray-50 hover:text-gray-600",
- secondary: "bg-gray-50 text-gray-900 hover:bg-gray-50",
- ghost: "hover:text-gray-900 hover:text-gray-900",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-10 px-4 py-2",
- sm: "px-3 py-1 text-sm",
- md: "px-4 py-2 text-base",
- lg: "px-6 py-3 text-lg",
- icon: "h-10 w-10",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-);
-
-export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean;
-}
-
-const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button";
- return (
-
- );
- }
-);
-Button.displayName = "Button";
-
-export { Button, buttonVariants };
diff --git a/Frontend/src/components/ui/calendar.tsx b/Frontend/src/components/ui/calendar.tsx
deleted file mode 100644
index 962e816..0000000
--- a/Frontend/src/components/ui/calendar.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import * as React from "react";
-import { ChevronLeft, ChevronRight } from "lucide-react";
-import { DayPicker } from "react-day-picker";
-
-import { cn } from "@/lib/utils";
-import { buttonVariants } from "@/components/ui/button";
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- ...props
-}: React.ComponentProps) {
- return (
- .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
- : "[&:has([aria-selected])]:rounded-md"
- ),
- day: cn(
- buttonVariants({ variant: "ghost" }),
- "size-8 p-0 font-normal aria-selected:opacity-100"
- ),
- day_range_start:
- "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
- day_range_end:
- "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
- day_selected:
- "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
- day_today: "bg-accent text-accent-foreground",
- day_outside:
- "day-outside text-muted-foreground aria-selected:text-muted-foreground",
- day_disabled: "text-muted-foreground opacity-50",
- day_range_middle:
- "aria-selected:bg-accent aria-selected:text-accent-foreground",
- day_hidden: "invisible",
- ...classNames,
- }}
- components={{
- IconLeft: ({ className, ...props }) => (
-
- ),
- IconRight: ({ className, ...props }) => (
-
- ),
- }}
- {...props}
- />
- );
-}
-
-export { Calendar };
diff --git a/Frontend/src/components/ui/card.tsx b/Frontend/src/components/ui/card.tsx
deleted file mode 100644
index 7727845..0000000
--- a/Frontend/src/components/ui/card.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-));
-Card.displayName = "Card";
-
-const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-));
-CardHeader.displayName = "CardHeader";
-
-const CardTitle = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-));
-CardTitle.displayName = "CardTitle";
-
-const CardDescription = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-));
-CardDescription.displayName = "CardDescription";
-
-const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-));
-CardContent.displayName = "CardContent";
-
-const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-));
-CardFooter.displayName = "CardFooter";
-
-export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardDescription,
- CardContent,
-};
diff --git a/Frontend/src/components/ui/dialog.tsx b/Frontend/src/components/ui/dialog.tsx
deleted file mode 100644
index b6cb28c..0000000
--- a/Frontend/src/components/ui/dialog.tsx
+++ /dev/null
@@ -1,246 +0,0 @@
-import * as React from "react"
-import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { XIcon } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-
-// Focus trap hook for managing focus within the dialog
-function useFocusTrap(enabled: boolean) {
- const containerRef = React.useRef(null)
- const previousFocusRef = React.useRef(null)
-
- React.useEffect(() => {
- if (!enabled || !containerRef.current) return
-
- const container = containerRef.current
- const focusableElements = container.querySelectorAll(
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
- )
- const firstElement = focusableElements[0] as HTMLElement
- const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement
-
- // Store the previously focused element
- previousFocusRef.current = document.activeElement as HTMLElement
-
- // Focus the first focusable element
- if (firstElement) {
- firstElement.focus()
- }
-
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === 'Tab') {
- if (event.shiftKey) {
- if (document.activeElement === firstElement) {
- event.preventDefault()
- lastElement.focus()
- }
- } else {
- if (document.activeElement === lastElement) {
- event.preventDefault()
- firstElement.focus()
- }
- }
- }
- }
-
- container.addEventListener('keydown', handleKeyDown)
-
- return () => {
- container.removeEventListener('keydown', handleKeyDown)
- // Restore focus to the previously focused element
- if (previousFocusRef.current) {
- previousFocusRef.current.focus()
- }
- }
- }, [enabled])
-
- return containerRef
-}
-
-function Dialog({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function DialogTrigger({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function DialogPortal({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function DialogClose({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function DialogOverlay({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-function DialogContent({
- className,
- children,
- ...props
-}: React.ComponentProps) {
- const [isOpen, setIsOpen] = React.useState(false)
- const contentRef = useFocusTrap(isOpen)
- const [titleId, setTitleId] = React.useState()
- const [descriptionId, setDescriptionId] = React.useState()
-
- // Handle Escape key to close dialog
- const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
- if (event.key === 'Escape') {
- event.preventDefault()
- // The close functionality is handled by Radix UI automatically
- }
- }, [])
-
- // Update open state when dialog state changes
- React.useEffect(() => {
- const content = contentRef.current
- if (!content) return
-
- const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- if (mutation.type === 'attributes' && mutation.attributeName === 'data-state') {
- const state = content.getAttribute('data-state')
- setIsOpen(state === 'open')
- }
- })
- })
-
- observer.observe(content, { attributes: true })
- return () => observer.disconnect()
- }, [contentRef])
-
- // Find title and description IDs from children
- React.useEffect(() => {
- const content = contentRef.current
- if (!content) return
-
- const title = content.querySelector('[data-slot="dialog-title"]')
- const description = content.querySelector('[data-slot="dialog-description"]')
-
- setTitleId(title?.id || undefined)
- setDescriptionId(description?.id || undefined)
- }, [children, contentRef])
-
- return (
-
-
-
- {children}
-
-
- Close
-
-
-
- )
-}
-
-function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function DialogTitle({
- className,
- ...props
-}: React.ComponentProps) {
- const titleId = React.useId()
-
- return (
- (null)}
- id={titleId}
- data-slot="dialog-title"
- className={cn("text-lg leading-none font-semibold", className)}
- {...props}
- />
- )
-}
-
-function DialogDescription({
- className,
- ...props
-}: React.ComponentProps) {
- const descriptionId = React.useId()
-
- return (
- (null)}
- id={descriptionId}
- data-slot="dialog-description"
- className={cn("text-muted-foreground text-sm", className)}
- {...props}
- />
- )
-}
-
-export {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogOverlay,
- DialogPortal,
- DialogTitle,
- DialogTrigger,
-}
diff --git a/Frontend/src/components/ui/dropdown-menu.tsx b/Frontend/src/components/ui/dropdown-menu.tsx
deleted file mode 100644
index 7c5d05d..0000000
--- a/Frontend/src/components/ui/dropdown-menu.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-import React from "react";
-import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
-import { Check, ChevronRight, Circle } from "lucide-react";
-import { cn } from "../../lib/utils";
-
-const DropdownMenu = DropdownMenuPrimitive.Root;
-const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
-const DropdownMenuGroup = DropdownMenuPrimitive.Group;
-const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
-const DropdownMenuSub = DropdownMenuPrimitive.Sub;
-const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
-
-const DropdownMenuSubTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef & {
- inset?: boolean;
- }
->(({ className, inset, children, ...props }, ref) => (
-
- {children}
-
-
-));
-DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName;
-
-const DropdownMenuSubContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName;
-
-const DropdownMenuContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, sideOffset = 4, ...props }, ref) => (
-
-
-
-));
-DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
-
-const DropdownMenuItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef & {
- inset?: boolean;
- }
->(({ className, inset, ...props }, ref) => (
-
-));
-DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
-
-const DropdownMenuCheckboxItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, checked, ...props }, ref) => (
-
-
-
-
-
-
- {children}
-
-));
-DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName;
-
-const DropdownMenuRadioItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
-
-
-
-
-
- {children}
-
-));
-DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
-
-const DropdownMenuLabel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef & {
- inset?: boolean;
- }
->(({ className, inset, ...props }, ref) => (
-
-));
-DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
-
-const DropdownMenuSeparator = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
-
-const DropdownMenuShortcut = ({
- className,
- ...props
-}: React.HTMLAttributes) => {
- return (
-
- );
-};
-DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
-
-export {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuCheckboxItem,
- DropdownMenuRadioItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuGroup,
- DropdownMenuPortal,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuRadioGroup,
-};
diff --git a/Frontend/src/components/ui/input.tsx b/Frontend/src/components/ui/input.tsx
deleted file mode 100644
index 6ed9422..0000000
--- a/Frontend/src/components/ui/input.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from "react";
-import { cn } from "../../lib/utils";
-
-const Input = React.forwardRef>(
- ({ className, type, ...props }, ref) => {
- return (
-
- );
- }
-);
-Input.displayName = "Input";
-
-export { Input };
diff --git a/Frontend/src/components/ui/label.tsx b/Frontend/src/components/ui/label.tsx
deleted file mode 100644
index f5e1d2c..0000000
--- a/Frontend/src/components/ui/label.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as LabelPrimitive from "@radix-ui/react-label";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "../../lib/utils";
-
-const labelVariants = cva(
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
-);
-
-const Label = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef &
- VariantProps
->(({ className, ...props }, ref) => (
-
-));
-
-export { Label };
diff --git a/Frontend/src/components/ui/popover.tsx b/Frontend/src/components/ui/popover.tsx
deleted file mode 100644
index 6d51b6c..0000000
--- a/Frontend/src/components/ui/popover.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import * as React from "react"
-import * as PopoverPrimitive from "@radix-ui/react-popover"
-
-import { cn } from "@/lib/utils"
-
-function Popover({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function PopoverTrigger({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-function PopoverContent({
- className,
- align = "center",
- sideOffset = 4,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
- )
-}
-
-function PopoverAnchor({
- ...props
-}: React.ComponentProps) {
- return
-}
-
-export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/Frontend/src/components/ui/scroll-area.tsx b/Frontend/src/components/ui/scroll-area.tsx
deleted file mode 100644
index 2cb3f46..0000000
--- a/Frontend/src/components/ui/scroll-area.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
-import { cn } from "../../lib/utils";
-
-const ScrollArea = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
-
- {children}
-
-
-
-
-
-));
-
-ScrollArea.displayName = "ScrollArea";
-
-export { ScrollArea };
diff --git a/Frontend/src/components/ui/select.tsx b/Frontend/src/components/ui/select.tsx
deleted file mode 100644
index f01b78d..0000000
--- a/Frontend/src/components/ui/select.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as SelectPrimitive from "@radix-ui/react-select";
-import { Check, ChevronDown, ChevronUp } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-
-const Select = SelectPrimitive.Root;
-
-const SelectGroup = SelectPrimitive.Group;
-
-const SelectValue = SelectPrimitive.Value;
-
-const SelectTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
- span]:line-clamp-1",
- className
- )}
- {...props}
- >
- {children}
-
-
-
-
-));
-SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
-
-const SelectScrollUpButton = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-));
-SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
-
-const SelectScrollDownButton = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-));
-SelectScrollDownButton.displayName =
- SelectPrimitive.ScrollDownButton.displayName;
-
-const SelectContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, position = "popper", ...props }, ref) => (
-
-
-
-
- {children}
-
-
-
-
-));
-SelectContent.displayName = SelectPrimitive.Content.displayName;
-
-const SelectLabel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-SelectLabel.displayName = SelectPrimitive.Label.displayName;
-
-const SelectItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
-
-
-
-
-
-
- {children}
-
-));
-SelectItem.displayName = SelectPrimitive.Item.displayName;
-
-const SelectSeparator = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
-
-export {
- Select,
- SelectGroup,
- SelectValue,
- SelectTrigger,
- SelectContent,
- SelectLabel,
- SelectItem,
- SelectSeparator,
- SelectScrollUpButton,
- SelectScrollDownButton,
-};
diff --git a/Frontend/src/components/ui/separator.tsx b/Frontend/src/components/ui/separator.tsx
deleted file mode 100644
index 5b6774d..0000000
--- a/Frontend/src/components/ui/separator.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as SeparatorPrimitive from "@radix-ui/react-separator";
-
-import { cn } from "@/lib/utils";
-
-const Separator = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(
- (
- { className, orientation = "horizontal", decorative = true, ...props },
- ref
- ) => (
-
- )
-);
-Separator.displayName = SeparatorPrimitive.Root.displayName;
-
-export { Separator };
diff --git a/Frontend/src/components/ui/slider.tsx b/Frontend/src/components/ui/slider.tsx
deleted file mode 100644
index 3b528d2..0000000
--- a/Frontend/src/components/ui/slider.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from "react";
-import * as SliderPrimitive from "@radix-ui/react-slider";
-import { cn } from "../../lib/utils";
-
-const Slider = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-
-
-
-));
-
-export { Slider };
diff --git a/Frontend/src/components/ui/switch.tsx b/Frontend/src/components/ui/switch.tsx
deleted file mode 100644
index d19165c..0000000
--- a/Frontend/src/components/ui/switch.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as SwitchPrimitives from "@radix-ui/react-switch";
-
-import { cn } from "@/lib/utils";
-
-const Switch = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-));
-Switch.displayName = SwitchPrimitives.Root.displayName;
-
-export { Switch };
diff --git a/Frontend/src/components/ui/tabs.tsx b/Frontend/src/components/ui/tabs.tsx
deleted file mode 100644
index 4859b71..0000000
--- a/Frontend/src/components/ui/tabs.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from "react";
-import * as TabsPrimitive from "@radix-ui/react-tabs";
-import { cn } from "../../lib/utils";
-
-const Tabs = TabsPrimitive.Root;
-
-const TabsList = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-TabsList.displayName = TabsPrimitive.List.displayName;
-
-const TabsTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
-
-const TabsContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-TabsContent.displayName = TabsPrimitive.Content.displayName;
-
-export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/Frontend/src/components/ui/textarea.tsx b/Frontend/src/components/ui/textarea.tsx
deleted file mode 100644
index 7f21b5e..0000000
--- a/Frontend/src/components/ui/textarea.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
-
-function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
- return (
-
- )
-}
-
-export { Textarea }
diff --git a/Frontend/src/components/user-nav.tsx b/Frontend/src/components/user-nav.tsx
deleted file mode 100644
index 9c4939f..0000000
--- a/Frontend/src/components/user-nav.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React from "react";
-import { useState } from "react";
-import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
-import { Button } from "./ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "./ui/dropdown-menu";
-import { useAuth } from "../context/AuthContext";
-import { Link } from "react-router-dom";
-
-export function UserNav() {
- const { user, isAuthenticated, logout } = useAuth();
- const [avatarError, setAvatarError] = useState(false);
-
- if (!isAuthenticated || !user) {
- return (
-
-
- Login
-
-
- Sign Up
-
-
- );
- }
-
- const handleAvatarError = () => {
- setAvatarError(true);
- };
-
- return (
-
-
-
-
-
- {user.user_metadata?.name?.charAt(0) || user.email?.charAt(0) || "U"}
-
-
-
-
-
-
-
{user.user_metadata?.name || "User"}
-
- {user.email}
-
-
-
-
-
-
- Dashboard
-
- Profile
- Settings
-
-
- Log out
-
-
- );
-}
diff --git a/Frontend/src/context/AuthContext.tsx b/Frontend/src/context/AuthContext.tsx
deleted file mode 100644
index 8588c41..0000000
--- a/Frontend/src/context/AuthContext.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- ReactNode,
- useEffect,
-} from "react";
-import { useNavigate } from "react-router-dom";
-import { supabase, User } from "../utils/supabase";
-
-interface AuthContextType {
- isAuthenticated: boolean;
- user: User | null;
- login: () => void;
- logout: () => void;
- checkUserOnboarding: (userToCheck?: User | null) => Promise<{ hasOnboarding: boolean; role: string | null }>;
-}
-
-const AuthContext = createContext(undefined);
-
-interface AuthProviderProps {
- children: ReactNode;
-}
-
-async function ensureUserInTable(user: any) {
- if (!user) return;
-
- // Add a simple cache to prevent repeated requests for the same user
- const cacheKey = `user_${user.id}`;
- if (sessionStorage.getItem(cacheKey)) {
- console.log("User already checked, skipping...");
- return;
- }
-
- try {
- console.log("Testing user table access for user:", user.id);
-
- // Just test if we can access the users table
- const { data, error } = await supabase
- .from("users")
- .select("id")
- .eq("id", user.id)
- .limit(1);
-
- if (error) {
- console.error("Error accessing users table:", error);
- return;
- }
-
- console.log("User table access successful, found:", data?.length || 0, "records");
-
- // Cache the result for 5 minutes to prevent repeated requests
- sessionStorage.setItem(cacheKey, "true");
- setTimeout(() => sessionStorage.removeItem(cacheKey), 5 * 60 * 1000);
-
- // For now, skip the insert to avoid 400 errors
- // We'll handle user creation during onboarding instead
-
- } catch (error) {
- console.error("Error in ensureUserInTable:", error);
- }
-}
-
-export const AuthProvider = ({ children }: AuthProviderProps) => {
- const [user, setUser] = useState(null);
- const [isAuthenticated, setIsAuthenticated] = useState(false);
- const [loading, setLoading] = useState(true);
- const [lastRequest, setLastRequest] = useState(0);
- const navigate = useNavigate();
-
- // Function to check if user has completed onboarding
- const checkUserOnboarding = async (userToCheck?: User | null) => {
- const userToUse = userToCheck || user;
- if (!userToUse) return { hasOnboarding: false, role: null };
-
- // Add rate limiting - only allow one request per 2 seconds
- const now = Date.now();
- if (now - lastRequest < 2000) {
- console.log("Rate limiting: skipping request");
- return { hasOnboarding: false, role: null };
- }
- setLastRequest(now);
-
- // Check if user has completed onboarding by looking for social profiles or brand data
- const { data: socialProfiles } = await supabase
- .from("social_profiles")
- .select("id")
- .eq("user_id", userToUse.id)
- .limit(1);
-
- const { data: brandData } = await supabase
- .from("brands")
- .select("id")
- .eq("user_id", userToUse.id)
- .limit(1);
-
- const hasOnboarding = (socialProfiles && socialProfiles.length > 0) || (brandData && brandData.length > 0);
-
- // Get user role
- const { data: userData } = await supabase
- .from("users")
- .select("role")
- .eq("id", userToUse.id)
- .single();
-
- return { hasOnboarding, role: userData?.role || null };
- };
-
- useEffect(() => {
- let mounted = true;
- console.log("AuthContext: Starting authentication check");
-
- // Add a timeout to prevent infinite loading
- const timeoutId = setTimeout(() => {
- if (mounted && loading) {
- console.log("AuthContext: Loading timeout reached, forcing completion");
- setLoading(false);
- }
- }, 3000); // 3 second timeout
-
- supabase.auth.getSession().then(async ({ data, error }) => {
- if (!mounted) return;
-
- if (error) {
- console.error("AuthContext: Error getting session", error);
- setLoading(false);
- return;
- }
-
- console.log("AuthContext: Session check result", { user: data.session?.user?.email, hasSession: !!data.session });
-
- setUser(data.session?.user || null);
- setIsAuthenticated(!!data.session?.user);
- if (data.session?.user) {
- console.log("AuthContext: Ensuring user in table");
- try {
- await ensureUserInTable(data.session.user);
- } catch (error) {
- console.error("AuthContext: Error ensuring user in table", error);
- }
- }
- setLoading(false);
- console.log("AuthContext: Initial loading complete");
- }).catch(error => {
- console.error("AuthContext: Error getting session", error);
- if (mounted) {
- setLoading(false);
- }
- });
-
- const { data: listener } = supabase.auth.onAuthStateChange(
- async (event, session) => {
- if (!mounted) return;
-
- console.log("AuthContext: Auth state change", { event, user: session?.user?.email });
-
- setUser(session?.user || null);
- setIsAuthenticated(!!session?.user);
-
- if (session?.user) {
- console.log("AuthContext: User authenticated");
- try {
- await ensureUserInTable(session.user);
- } catch (error) {
- console.error("AuthContext: Error ensuring user in table", error);
- }
- setLoading(false);
- } else {
- // User logged out
- console.log("AuthContext: User logged out");
- setLoading(false);
- }
- }
- );
-
- return () => {
- mounted = false;
- clearTimeout(timeoutId);
- listener.subscription.unsubscribe();
- };
- }, []);
-
- const login = () => {
- setIsAuthenticated(true);
- navigate("/dashboard");
- };
-
- const logout = async () => {
- await supabase.auth.signOut();
- setUser(null);
- setIsAuthenticated(false);
- navigate("/");
- };
-
- if (loading) {
- return (
-
-
Loading...
-
(If this is taking too long, try refreshing the page.)
-
setLoading(false)}
- className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
- >
- Continue Anyway
-
-
- );
- }
-
- return (
-
- {children}
-
- );
-};
-export const useAuth = () => {
- const context = useContext(AuthContext);
- if (!context) {
- throw new Error("useAuth must be used within an AuthProvider");
- }
- return context;
-};
\ No newline at end of file
diff --git a/Frontend/src/hooks/useCollaborationState.ts b/Frontend/src/hooks/useCollaborationState.ts
deleted file mode 100644
index 174a6e6..0000000
--- a/Frontend/src/hooks/useCollaborationState.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { useReducer, useCallback } from 'react';
-
-// Types
-export interface FilterState {
- niche: string;
- audienceSize: string;
- collaborationType: string;
- location: string;
-}
-
-export interface ModalState {
- newCollaboration: boolean;
- aiSearch: boolean;
-}
-
-export interface CollaborationState {
- modals: ModalState;
- filters: FilterState;
-}
-
-// Action Types
-export type CollaborationAction =
- | { type: 'OPEN_MODAL'; modal: keyof ModalState }
- | { type: 'CLOSE_MODAL'; modal: keyof ModalState }
- | { type: 'UPDATE_FILTER'; filter: keyof FilterState; value: string }
- | { type: 'RESET_FILTERS' }
- | { type: 'RESET_ALL' };
-
-// Initial State
-const initialState: CollaborationState = {
- modals: {
- newCollaboration: false,
- aiSearch: false,
- },
- filters: {
- niche: 'all',
- audienceSize: 'all',
- collaborationType: 'all',
- location: 'all',
- },
-};
-
-// Reducer Function
-function collaborationReducer(
- state: CollaborationState,
- action: CollaborationAction
-): CollaborationState {
- switch (action.type) {
- case 'OPEN_MODAL':
- return {
- ...state,
- modals: {
- ...state.modals,
- [action.modal]: true,
- },
- };
-
- case 'CLOSE_MODAL':
- return {
- ...state,
- modals: {
- ...state.modals,
- [action.modal]: false,
- },
- };
-
- case 'UPDATE_FILTER':
- return {
- ...state,
- filters: {
- ...state.filters,
- [action.filter]: action.value,
- },
- };
-
- case 'RESET_FILTERS':
- return {
- ...state,
- filters: initialState.filters,
- };
-
- case 'RESET_ALL':
- return initialState;
-
- default:
- return state;
- }
-}
-
-// Custom Hook
-export function useCollaborationState() {
- const [state, dispatch] = useReducer(collaborationReducer, initialState);
-
- // Modal Actions
- const openModal = useCallback((modal: keyof ModalState) => {
- dispatch({ type: 'OPEN_MODAL', modal });
- }, []);
-
- const closeModal = useCallback((modal: keyof ModalState) => {
- dispatch({ type: 'CLOSE_MODAL', modal });
- }, []);
-
- const openNewCollaborationModal = useCallback(() => {
- openModal('newCollaboration');
- }, [openModal]);
-
- const closeNewCollaborationModal = useCallback(() => {
- closeModal('newCollaboration');
- }, [closeModal]);
-
- const openAiSearchModal = useCallback(() => {
- openModal('aiSearch');
- }, [openModal]);
-
- const closeAiSearchModal = useCallback(() => {
- closeModal('aiSearch');
- }, [closeModal]);
-
- // Filter Actions
- const updateFilter = useCallback((filter: keyof FilterState, value: string) => {
- dispatch({ type: 'UPDATE_FILTER', filter, value });
- }, []);
-
- const resetFilters = useCallback(() => {
- dispatch({ type: 'RESET_FILTERS' });
- }, []);
-
- const resetAll = useCallback(() => {
- dispatch({ type: 'RESET_ALL' });
- }, []);
-
- // Computed Values
- const hasActiveFilters = Object.values(state.filters).some(value => value !== 'all');
- const activeFiltersCount = Object.values(state.filters).filter(value => value !== 'all').length;
-
- return {
- // State
- state,
- modals: state.modals,
- filters: state.filters,
-
- // Modal Actions
- openModal,
- closeModal,
- openNewCollaborationModal,
- closeNewCollaborationModal,
- openAiSearchModal,
- closeAiSearchModal,
-
- // Filter Actions
- updateFilter,
- resetFilters,
- resetAll,
-
- // Computed Values
- hasActiveFilters,
- activeFiltersCount,
- };
-}
\ No newline at end of file
diff --git a/Frontend/src/index.css b/Frontend/src/index.css
deleted file mode 100644
index f2a93bb..0000000
--- a/Frontend/src/index.css
+++ /dev/null
@@ -1,181 +0,0 @@
-@import "tailwindcss";
-@import "tw-animate-css";
-
-@custom-variant dark (&:is(.dark *));
-
-:root {
- --radius: 0.625rem;
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
-}
-
-.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
-}
-
-@theme inline {
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --color-card: var(--card);
- --color-card-foreground: var(--card-foreground);
- --color-popover: var(--popover);
- --color-popover-foreground: var(--popover-foreground);
- --color-primary: var(--primary);
- --color-primary-foreground: var(--primary-foreground);
- --color-secondary: var(--secondary);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-muted: var(--muted);
- --color-muted-foreground: var(--muted-foreground);
- --color-accent: var(--accent);
- --color-accent-foreground: var(--accent-foreground);
- --color-destructive: var(--destructive);
- --color-border: var(--border);
- --color-input: var(--input);
- --color-ring: var(--ring);
- --color-chart-1: var(--chart-1);
- --color-chart-2: var(--chart-2);
- --color-chart-3: var(--chart-3);
- --color-chart-4: var(--chart-4);
- --color-chart-5: var(--chart-5);
- --color-sidebar: var(--sidebar);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-ring: var(--sidebar-ring);
-}
-
-@layer base {
- * {
- @apply border-border outline-ring/50;
- }
- body {
- @apply bg-background text-foreground;
- }
-}
-
-/* Custom Animations */
-@keyframes gradient {
- 0% {
- background-position: 0% 50%;
- }
- 50% {
- background-position: 100% 50%;
- }
- 100% {
- background-position: 0% 50%;
- }
-}
-
-@keyframes float {
- 0%, 100% {
- transform: translateY(0px);
- }
- 50% {
- transform: translateY(-10px);
- }
-}
-
-@keyframes glow {
- 0%, 100% {
- box-shadow: 0 0 20px rgba(147, 51, 234, 0.3);
- }
- 50% {
- box-shadow: 0 0 40px rgba(147, 51, 234, 0.6);
- }
-}
-
-.animate-gradient {
- background-size: 200% 200%;
- animation: gradient 3s ease infinite;
-}
-
-.animate-float {
- animation: float 3s ease-in-out infinite;
-}
-
-.animate-glow {
- animation: glow 2s ease-in-out infinite;
-}
-
-/* 3D Text Effect */
-.text-3d {
- text-shadow:
- 0 1px 0 #ccc,
- 0 2px 0 #c9c9c9,
- 0 3px 0 #bbb,
- 0 4px 0 #b9b9b9,
- 0 5px 0 #aaa,
- 0 6px 1px rgba(0,0,0,.1),
- 0 0 5px rgba(0,0,0,.1),
- 0 1px 3px rgba(0,0,0,.3),
- 0 3px 5px rgba(0,0,0,.2),
- 0 5px 10px rgba(0,0,0,.25),
- 0 10px 10px rgba(0,0,0,.2),
- 0 20px 20px rgba(0,0,0,.15);
-}
diff --git a/Frontend/src/lib/useChat.tsx b/Frontend/src/lib/useChat.tsx
deleted file mode 100644
index 771c562..0000000
--- a/Frontend/src/lib/useChat.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-import React, {
- createContext,
- useContext,
- useEffect,
- useRef,
- useState,
-} from "react";
-import { useDispatch } from "react-redux";
-import {
- addChats,
- addMessage,
- addOldMessages,
- Message,
- markChatAsSeen as reduxMarkChatAsSeen,
- updateUserDetails,
- markMessageAsSeen as reduxMarkMessageAsSeen,
- setSelectedChat,
-} from "@/redux/chatSlice";
-import { API_URL } from "@/lib/utils";
-import axios from "axios";
-
-interface ChatContextType {
- sendMessage: (receiverId: string, message: string) => void;
- isConnected: boolean;
- fetchChatList: () => Promise;
- fetchChatMessages: (
- chatListId: string,
- lastFetched: number
- ) => Promise;
- markChatAsSeen: (chatListId: string) => Promise;
- fetchUserDetails: (targetUserId: string, chatListId: string) => Promise;
- markMessageAsSeen: (chatListId: string, messageId: string) => Promise;
- createChatWithMessage: (
- username: string,
- message: string
- ) => Promise;
-}
-
-const ChatContext = createContext(undefined);
-
-export const ChatProvider: React.FC<{
- userId: string;
- children: React.ReactNode;
-}> = ({ userId, children }) => {
- const ws = useRef(null);
- const [isConnected, setIsConnected] = useState(false);
- const dispatch = useDispatch();
-
- useEffect(() => {
- if (!userId) return;
-
- const websocket = new WebSocket(
- `ws://${API_URL.replace(/^https?:\/\//, "")}/chat/ws/${userId}`
- );
-
- websocket.onopen = () => {
- console.log("WebSocket Connected");
- setIsConnected(true);
- };
-
- websocket.onmessage = (event) => {
- const data = JSON.parse(event.data);
- console.log("Message received:", data);
-
- if (
- data.eventType === "NEW_MESSAGE_RECEIVED" ||
- data.eventType === "NEW_MESSAGE_DELIVERED" ||
- data.eventType === "NEW_MESSAGE_SENT"
- ) {
- dispatch(
- addMessage({
- chatListId: data.chatListId,
- message: data,
- })
- );
- } else if (data.eventType === "CHAT_MESSAGES_READ") {
- dispatch(
- reduxMarkChatAsSeen({
- chatListId: data.chatListId,
- })
- );
- } else if (data.eventType === "MESSAGE_READ") {
- dispatch(
- reduxMarkMessageAsSeen({
- chatListId: data.chatListId,
- messageId: data.messageId,
- })
- );
- }
- };
-
- websocket.onclose = () => {
- console.log("WebSocket Disconnected");
- setIsConnected(false);
- };
-
- ws.current = websocket;
-
- return () => {
- if (ws.current) ws.current.close();
- };
- }, [userId, dispatch]);
-
- const sendMessage = (receiverId: string, message: string) => {
- if (ws.current && isConnected) {
- ws.current.send(
- JSON.stringify({
- event_type: "SEND_MESSAGE",
- receiver_id: receiverId,
- message: message,
- })
- );
- }
- };
-
- const fetchChatList = async () => {
- try {
- const response = await axios.get(`${API_URL}/chat/chat_list/${userId}`);
- dispatch(addChats(response.data));
- } catch (error) {
- console.error("Error fetching chat list:", error);
- }
- };
-
- const fetchChatMessages = async (chatListId: string, lastFetched: number) => {
- try {
- const response = await axios.get(
- `${API_URL}/chat/messages/${userId}/${chatListId}`,
- {
- params: {
- last_fetched: lastFetched,
- },
- }
- );
- dispatch(
- addOldMessages({
- chatListId: chatListId,
- messages: response.data,
- })
- );
- if (response.data.length === 0) return false;
- return true;
- } catch (error) {
- console.error("Error fetching chat messages:", error);
- return false;
- }
- };
-
- const markChatAsSeen = async (chatListId: string) => {
- try {
- await axios.put(API_URL + `/chat/read/${userId}/${chatListId}`);
- } catch (error) {
- console.error("Error marking chat as seen:", error);
- }
- };
-
- const fetchUserDetails = async (targetUserId: string, chatListId: string) => {
- try {
- const response = await axios.get(
- `${API_URL}/chat/user_name/${targetUserId}`
- );
- dispatch(
- updateUserDetails({
- chatListId: chatListId,
- username: response.data.username,
- profileImage: response.data.profileImage,
- })
- );
- } catch (error) {
- console.error("Error fetching username:", error);
- }
- };
-
- const markMessageAsSeen = async (chatListId: string, messageId: string) => {
- try {
- await axios.put(
- `${API_URL}/chat/read/${userId}/${chatListId}/${messageId}`
- );
- } catch (error) {
- console.error("Error marking message as seen:", error);
- }
- };
-
- const createChatWithMessage = async (username: string, message: string) => {
- try {
- const response = await axios.post(
- `${API_URL}/chat/new_chat/${userId}/${username}`,
- {
- message,
- }
- );
- const chatListId = response.data.chatListId;
- dispatch(setSelectedChat(chatListId));
- if (response.data.isChatListExists) {
- return false;
- }
- return true;
- } catch (error) {
- console.error("Error creating chat with message:", error);
-
- return false;
- }
- };
-
- return (
-
- {isConnected ? children : null}
-
- );
-};
-
-export const useChat = (): ChatContextType => {
- const context = useContext(ChatContext);
- if (!context) {
- throw new Error("useChat must be used within a ChatProvider");
- }
- return context;
-};
diff --git a/Frontend/src/lib/utils.ts b/Frontend/src/lib/utils.ts
deleted file mode 100644
index 43fc355..0000000
--- a/Frontend/src/lib/utils.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { clsx, type ClassValue } from "clsx";
-import { twMerge } from "tailwind-merge";
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
-
-export const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
diff --git a/Frontend/src/main.tsx b/Frontend/src/main.tsx
deleted file mode 100644
index 18b97e0..0000000
--- a/Frontend/src/main.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { StrictMode } from "react";
-import { createRoot } from "react-dom/client";
-import "./index.css";
-import { Provider } from "react-redux";
-import App from "./App.tsx";
-import store from "./redux/store.ts";
-
-createRoot(document.getElementById("root")!).render(
- //
-
-
-
- // ,
-);
diff --git a/Frontend/src/pages/Analytics.tsx b/Frontend/src/pages/Analytics.tsx
deleted file mode 100644
index 8ae90fe..0000000
--- a/Frontend/src/pages/Analytics.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react'
-
-function Analytics() {
- return (
- Analytics
- )
-}
-
-export default Analytics
\ No newline at end of file
diff --git a/Frontend/src/pages/BasicDetails.tsx b/Frontend/src/pages/BasicDetails.tsx
deleted file mode 100644
index d72e0ef..0000000
--- a/Frontend/src/pages/BasicDetails.tsx
+++ /dev/null
@@ -1,577 +0,0 @@
-import { Button } from "../components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "../components/ui/card";
-import { Input } from "../components/ui/input";
-import { Label } from "../components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "../components/ui/select";
-import {
- Instagram,
- Youtube,
- Twitter,
- BookText as TikTok,
- Globe,
- ChevronRight,
- ChevronLeft,
- Rocket,
- Check,
-} from "lucide-react";
-import { motion, AnimatePresence } from "framer-motion";
-import { useState, useEffect } from "react";
-import { useParams } from "react-router-dom";
-import { UserNav } from "../components/user-nav";
-import { Link } from "react-router-dom";
-import { ModeToggle } from "../components/mode-toggle";
-
-export default function BasicDetails() {
- const { user } = useParams();
- const [step, setStep] = useState(0);
- const [animationDirection, setAnimationDirection] = useState(0);
-
- const totalSteps = user === "influencer" ? 3 : 2;
- const nextStep = () => {
- if ((user === "influencer" && step < 2) || (user === "brand" && step < 1)) {
- setAnimationDirection(1);
- setTimeout(() => {
- setStep((prev) => prev + 1);
- }, 50);
- }
- };
-
- const prevStep = () => {
- if (step > 0) {
- setAnimationDirection(-1);
- setTimeout(() => {
- setStep((prev) => prev - 1);
- }, 50);
- }
- };
-
- useEffect(() => {
- // Reset animation direction after animation completes
- const timer = setTimeout(() => {
- setAnimationDirection(0);
- }, 500);
- return () => clearTimeout(timer);
- }, [step]);
-
- const InfluencerBasicDetails = () => (
-
-
-
- Email
-
-
-
- Phone Number
-
-
-
- Content Category
-
-
-
-
-
- Lifestyle
- Technology
- Fashion
- Gaming
- Food
- Travel
- Fitness
- Education
-
-
-
-
- );
-
- const InfluencerSocialMedia = () => (
-
- );
-
- const InfluencerAudience = () => (
-
-
- Total Audience Size
-
-
-
- Average Engagement Rate (%)
-
-
-
- Primary Platform
-
-
-
-
-
- Instagram
- YouTube
- TikTok
- Twitter
-
-
-
-
- Primary Audience Age Range
-
-
-
-
-
- 13-17
- 18-24
- 25-34
- 35-44
- 45+
-
-
-
-
- );
-
- const BrandBasicDetails = () => (
-
-
-
- Brand Information
-
- Company Name
-
-
-
- Company Website
-
-
-
-
- Industry
-
-
-
-
-
- Fashion
- Technology
- Food & Beverage
- Health & Wellness
- Beauty
- Entertainment
-
-
-
-
- Company Size
-
-
-
-
-
- 1-10 employees
- 11-50 employees
- 51-200 employees
- 201-500 employees
- 501+ employees
-
-
-
-
-
- Monthly Marketing Budget
-
-
-
-
-
- $0 - $5,000
- $5,001 - $10,000
- $10,001 - $50,000
- $50,001+
-
-
-
-
- );
-
- const BrandCampaignPreferences = () => (
-
-
-
- Campaign Settings
-
- Target Audience Age Range
-
-
-
-
-
- 13-17
- 18-24
- 25-34
- 35-44
- 45+
-
-
-
-
- Preferred Platforms
-
-
-
-
-
- Instagram
- YouTube
- TikTok
- Twitter
-
-
-
-
- Primary Campaign Goals
-
-
-
-
-
- Brand Awareness
- Direct Sales
- Community Engagement
- Brand Loyalty
-
-
-
-
- );
-
- const getStepContent = () => {
- if (user === "influencer") {
- switch (step) {
- case 0:
- return {
- title: "Basic Details",
- description: "Let's start with your personal information",
- content: ,
- };
- case 1:
- return {
- title: "Social Media Profiles",
- description: "Connect your social media accounts",
- content: ,
- };
- case 2:
- return {
- title: "Audience Information",
- description: "Tell us about your audience and engagement",
- content: ,
- };
- }
- } else {
- switch (step) {
- case 0:
- return {
- title: "Company Information",
- description: "Tell us about your brand",
- content: ,
- };
- case 1:
- return {
- title: "Campaign Preferences",
- description: "Define your target audience and campaign goals",
- content: ,
- };
- }
- }
- };
-
- const currentStep = getStepContent();
- const variants = {
- enter: (direction: number) => {
- return {
- x: direction > 0 ? 300 : -300,
- opacity: 0,
- };
- },
- center: {
- x: 0,
- opacity: 1,
- },
- exit: (direction: number) => {
- return {
- x: direction < 0 ? 300 : -300,
- opacity: 0,
- };
- },
- };
-
- const resetForm = () => {
- setStep(0);
- setAnimationDirection(0);
-
- document.querySelectorAll("input").forEach((input) => (input.value = ""));
- document
- .querySelectorAll("select")
- .forEach((select) => (select.value = ""));
- };
-
- return (
- <>
-
-
-
-
-
- Inpact
-
-
-
- Need help?
-
- Contact support
-
-
-
-
-
-
-
-
- {/* Progress indicator */}
-
-
-
- Step {step + 1} of {totalSteps}
-
-
- {Math.round(((step + 1) / totalSteps) * 100)}% Complete
-
-
-
-
-
-
- {Array.from({ length: totalSteps }).map((_, index) => (
-
-
- {index < step ? (
-
- ) : (
-
- {index + 1}
-
- )}
-
-
- ))}
-
-
-
-
-
-
- {currentStep?.title}
-
-
- {currentStep?.description}
-
-
-
-
-
- {currentStep?.content}
-
-
-
-
-
-
- Back
-
-
-
-
- {Array.from({ length: totalSteps }).map(
- (_, index) => (
-
- )
- )}
-
-
-
-
- {step === totalSteps - 1 ? "Complete" : "Next"}
-
-
-
-
-
-
-
-
- Need to start over?{" "}
-
- Reset form
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/Frontend/src/pages/Brand/Dashboard.tsx b/Frontend/src/pages/Brand/Dashboard.tsx
deleted file mode 100644
index 023c77b..0000000
--- a/Frontend/src/pages/Brand/Dashboard.tsx
+++ /dev/null
@@ -1,380 +0,0 @@
-import Chat from "@/components/chat/chat";
-import { Button } from "../../components/ui/button";
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
-} from "../../components/ui/card";
-import { Input } from "../../components/ui/input";
-import {
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
-} from "../../components/ui/tabs";
-import {
- BarChart3,
- Users,
- MessageSquareMore,
- TrendingUp,
- Search,
- Bell,
- UserCircle,
- FileText,
- Send,
- Clock,
- CheckCircle2,
- XCircle,
- BarChart,
- ChevronRight,
- FileSignature,
- LineChart,
- Activity,
- Rocket,
-} from "lucide-react";
-import { CreatorMatches } from "../../components/dashboard/creator-matches";
-import { useState } from "react";
-
-const Dashboard = () => {
- // Mock sponsorships for selection (replace with real API call if needed)
- const sponsorships = [
- { id: "1", title: "Summer Collection" },
- { id: "2", title: "Tech Launch" },
- { id: "3", title: "Fitness Drive" },
- ];
- const [selectedSponsorship, setSelectedSponsorship] = useState("");
-
- return (
- <>
-
- {/* Navigation */}
-
-
-
-
-
-
- Inpact
-
-
- Brand
-
-
-
-
-
-
-
-
- {/* Header */}
-
-
- Brand Dashboard
-
-
- Discover and collaborate with creators that match your brand
-
-
-
- {/* Search */}
-
-
-
-
-
- {/* Main Content */}
-
-
- Discover
- Contracts
- Messages
- Tracking
-
-
- {/* Discover Tab */}
-
- {/* Stats */}
-
-
-
-
- Active Creators
-
-
-
-
- 12,234
-
- +180 from last month
-
-
-
-
-
-
- Avg. Engagement
-
-
-
-
- 4.5%
-
- +0.3% from last month
-
-
-
-
-
-
- Active Campaigns
-
-
-
-
- 24
-
- 8 pending approval
-
-
-
-
-
-
- Messages
-
-
-
-
- 12
-
- 3 unread messages
-
-
-
-
-
- {/* Creator Recommendations */}
-
-
-
- Matched Creators for Your Campaign
-
-
-
- Select Campaign:
-
-
-
-
-
-
- {/* Contracts Tab */}
-
-
-
- Active Contracts
-
-
-
- New Contract
-
-
-
-
- {[1, 2, 3].map((i) => (
-
-
-
-
-
-
- Summer Collection Campaign
-
-
- with Alex Rivera
-
-
-
-
- Due in 12 days
-
-
-
-
-
-
- Active
-
-
- $2,400
-
-
-
-
-
-
-
- View Contract
-
-
-
- Message
-
-
-
-
-
- ))}
-
-
-
- {/* Messages Tab */}
-
-
-
-
- {/* Tracking Tab */}
-
-
-
-
-
- Total Reach
-
-
-
-
- 2.4M
-
- Across all campaigns
-
-
-
-
-
-
- Engagement Rate
-
-
-
-
- 5.2%
-
- Average across creators
-
-
-
-
-
- ROI
-
-
-
- 3.8x
-
- Last 30 days
-
-
-
-
-
-
- Active Posts
-
-
-
-
- 156
-
- Across platforms
-
-
-
-
-
-
-
- Campaign Performance
-
-
- {[1, 2, 3].map((i) => (
-
-
-
-
-
-
Summer Collection
-
- with Sarah Parker
-
-
-
-
-
458K Reach
-
- 6.2% Engagement
-
-
-
-
-
-
- 12 Posts Live
-
-
-
- 2 Pending
-
-
-
- ))}
-
-
-
-
-
-
- >
- );
-};
-
-export default Dashboard;
diff --git a/Frontend/src/pages/CollaborationDetails.tsx b/Frontend/src/pages/CollaborationDetails.tsx
deleted file mode 100644
index f0c29f9..0000000
--- a/Frontend/src/pages/CollaborationDetails.tsx
+++ /dev/null
@@ -1,900 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useParams, useNavigate, Link } from "react-router-dom";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
-import { Button } from "../components/ui/button";
-import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar";
-import { Badge } from "../components/ui/badge";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
-import { Input } from "../components/ui/input";
-import { Textarea } from "../components/ui/textarea";
-import { Separator } from "../components/ui/separator";
-import { ModeToggle } from "../components/mode-toggle";
-import { UserNav } from "../components/user-nav";
-import {
- ArrowLeft,
- MessageSquare,
- Calendar,
- CheckCircle,
- Clock,
- FileText,
- Users,
- BarChart3,
- Send,
- Edit,
- Download,
- Eye,
- MoreHorizontal,
- Phone,
- Mail,
- MapPin,
- ExternalLink,
- Star,
- TrendingUp,
- Activity,
- LayoutDashboard,
- Briefcase,
- Search,
- Rocket,
- X
-} from "lucide-react";
-import { activeCollabsMock } from "../components/collaboration-hub/activeCollabsMockData";
-import CollaborationOverviewTab from "../components/collaboration-hub/CollaborationOverviewTab";
-import CollaborationMessagesTab from "../components/collaboration-hub/CollaborationMessagesTab";
-import CollaborationTimelineTab from "../components/collaboration-hub/CollaborationTimelineTab";
-import CollaboratorSidebar from "../components/collaboration-hub/CollaboratorSidebar";
-import CollaborationQuickActions from "../components/collaboration-hub/CollaborationQuickActions";
-import CollaborationProjectStats from "../components/collaboration-hub/CollaborationProjectStats";
-
-interface Message {
- id: number;
- sender: string;
- content: string;
- timestamp: string;
- isOwn: boolean;
-}
-
-interface Deliverable {
- id: number;
- title: string;
- description: string;
- status: "pending" | "in-progress" | "completed" | "review";
- dueDate: string;
- assignedTo: string;
- files?: string[];
-}
-
-interface Milestone {
- id: number;
- title: string;
- description: string;
- dueDate: string;
- status: "upcoming" | "in-progress" | "completed";
- progress: number;
-}
-
-const mockMessages: Message[] = [
- {
- id: 1,
- sender: "GadgetGuru",
- content: "Hey! I've started working on the unboxing video. Should have the first draft ready by tomorrow.",
- timestamp: "2024-06-10 14:30",
- isOwn: false
- },
- {
- id: 2,
- sender: "You",
- content: "Perfect! Looking forward to seeing it. Any specific angles you want to focus on?",
- timestamp: "2024-06-10 15:45",
- isOwn: true
- },
- {
- id: 3,
- sender: "GadgetGuru",
- content: "I'm thinking close-ups of the packaging and then a reveal shot. Also planning to include some B-roll of the setup process.",
- timestamp: "2024-06-10 16:20",
- isOwn: false
- }
-];
-
-const mockDeliverables: Deliverable[] = [
- {
- id: 1,
- title: "Unboxing Video",
- description: "Main unboxing video showcasing the product features",
- status: "in-progress",
- dueDate: "2024-06-12",
- assignedTo: "GadgetGuru",
- files: ["unboxing_draft_v1.mp4"]
- },
- {
- id: 2,
- title: "Thumbnail Design",
- description: "Eye-catching thumbnail for the video",
- status: "completed",
- dueDate: "2024-06-10",
- assignedTo: "GadgetGuru",
- files: ["thumbnail_final.png"]
- },
- {
- id: 3,
- title: "Social Media Posts",
- description: "Instagram and TikTok posts promoting the collaboration",
- status: "pending",
- dueDate: "2024-06-15",
- assignedTo: "GadgetGuru"
- }
-];
-
-const mockMilestones: Milestone[] = [
- {
- id: 1,
- title: "Project Kickoff",
- description: "Initial meeting and project setup",
- dueDate: "2024-06-01",
- status: "completed",
- progress: 100
- },
- {
- id: 2,
- title: "Content Creation",
- description: "Video production and editing",
- dueDate: "2024-06-12",
- status: "in-progress",
- progress: 65
- },
- {
- id: 3,
- title: "Review & Approval",
- description: "Content review and final approval",
- dueDate: "2024-06-14",
- status: "upcoming",
- progress: 0
- },
- {
- id: 4,
- title: "Publication",
- description: "Video goes live on all platforms",
- dueDate: "2024-06-15",
- status: "upcoming",
- progress: 0
- }
-];
-
-export default function CollaborationDetails() {
- const { id } = useParams<{ id: string }>();
- const navigate = useNavigate();
- const [newMessage, setNewMessage] = useState("");
- const [activeTab, setActiveTab] = useState("overview");
- const [showContractModal, setShowContractModal] = useState(false);
- const [messageStyle, setMessageStyle] = useState("professional");
- const [showStyleOptions, setShowStyleOptions] = useState(false);
- const [customStyle, setCustomStyle] = useState("");
- const [isEditingUpdate, setIsEditingUpdate] = useState(false);
- const [editedUpdate, setEditedUpdate] = useState("");
- const [showDeliverableModal, setShowDeliverableModal] = useState(false);
- const [selectedDeliverable, setSelectedDeliverable] = useState(null);
-
- // Find the collaboration data
- const collaboration = activeCollabsMock.find(collab => collab.id === parseInt(id || "1"));
-
- if (!collaboration) {
- return (
-
-
-
Collaboration Not Found
-
The collaboration you're looking for doesn't exist.
-
navigate("/dashboard/collaborations")}>
- Back to Collaborations
-
-
-
- );
- }
-
- const getStatusColor = (status: string) => {
- switch (status) {
- case "In Progress": return "bg-blue-100 text-blue-700";
- case "Awaiting Response": return "bg-yellow-100 text-yellow-700";
- case "Completed": return "bg-green-100 text-green-700";
- default: return "bg-gray-100 text-gray-700";
- }
- };
-
- const getDeliverableStatusColor = (status: string) => {
- switch (status) {
- case "completed": return "bg-green-100 text-green-700";
- case "in-progress": return "bg-blue-100 text-blue-700";
- case "review": return "bg-yellow-100 text-yellow-700";
- case "pending": return "bg-gray-100 text-gray-700";
- default: return "bg-gray-100 text-gray-700";
- }
- };
-
- const getMilestoneStatusColor = (status: string) => {
- switch (status) {
- case "completed": return "bg-green-100 text-green-700";
- case "in-progress": return "bg-blue-100 text-blue-700";
- case "upcoming": return "bg-gray-100 text-gray-700";
- default: return "bg-gray-100 text-gray-700";
- }
- };
-
- const handleSendMessage = () => {
- if (newMessage.trim()) {
- // In a real app, this would send the message to the backend
- setNewMessage("");
- }
- };
-
- const handleViewContract = () => {
- setShowContractModal(true);
- };
-
- // Mock contract URL - in a real app, this would come from the collaboration data
- const contractUrl = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf";
-
- // Message style options for AI enhancement
- const messageStyles = [
- { value: "professional", label: "Professional", description: "Formal and business-like" },
- { value: "casual", label: "Casual", description: "Friendly and relaxed" },
- { value: "polite", label: "Polite", description: "Courteous and respectful" },
- { value: "concise", label: "Concise", description: "Brief and to the point" },
- { value: "enthusiastic", label: "Enthusiastic", description: "Energetic and positive" },
- { value: "constructive", label: "Constructive", description: "Helpful and solution-focused" }
- ];
-
- /**
- * AI Message Style Enhancement Feature(Not implemented yet)
- * i am putting this here for future reference for contributors...
- * This feature allows users to enhance their message content using AI to match different communication styles.
- *
- * Requirements for future development:
- * 1. Integration with LLM API (OpenAI, Anthropic, etc.) to generate styled messages
- * 2. Real-time message transformation as user types or selects style
- * 3. Support for custom style descriptions (user-defined tone/approach)
- * 4. Context awareness - consider collaboration history and relationship
- * 5. Style suggestions based on message content and collaboration stage
- * 6. Option to preview changes before applying
- * 7. Learning from user preferences and successful communication patterns
- * 8. Integration with collaboration analytics to suggest optimal communication timing
- *
- * Technical considerations:
- * - API rate limiting and error handling
- * - Caching of common style transformations
- * - Privacy and data security for message content
- * - Real-time collaboration features (typing indicators, etc.)
- */
- const handleStyleChange = (style: string) => {
- setMessageStyle(style);
- setShowStyleOptions(false);
-
- // TODO: Implement AI message transformation
- // This would call an LLM API to transform the current message
- // Example API call structure:
- // const transformedMessage = await transformMessageStyle(newMessage, style);
- // setNewMessage(transformedMessage);
- };
-
- const handleCustomStyle = () => {
- if (customStyle.trim()) {
- // TODO: Implement custom style transformation
- // This would use the custom style description to guide the AI transformation
- setCustomStyle("");
- setShowStyleOptions(false);
- }
- };
-
- const handleEditUpdate = () => {
- setEditedUpdate(collaboration.latestUpdate);
- setIsEditingUpdate(true);
- };
-
- const handleSaveUpdate = () => {
- // TODO: Implement API call to save the updated latest update
- // This would update the collaboration's latest update in the backend
- // For now, we'll just close the edit mode
- // In a real app, you would update the collaboration object here
- setIsEditingUpdate(false);
- };
-
- const handleCancelEdit = () => {
- setIsEditingUpdate(false);
- setEditedUpdate("");
- };
-
- const handleViewDeliverable = (deliverable: Deliverable) => {
- setSelectedDeliverable(deliverable);
- setShowDeliverableModal(true);
- };
-
- const handleCloseDeliverableModal = () => {
- setShowDeliverableModal(false);
- setSelectedDeliverable(null);
- };
-
- return (
-
- {/* Main Header */}
-
-
-
-
-
Inpact
-
-
- {[
- { to: "/dashboard", icon: LayoutDashboard, label: "Dashboard" },
- { to: "/dashboard/sponsorships", icon: Briefcase, label: "Sponsorships" },
- { to: "/dashboard/collaborations", icon: Users, label: "Collaborations" },
- { to: "/dashboard/contracts", icon: FileText, label: "Contracts" },
- { to: "/dashboard/analytics", icon: BarChart3, label: "Analytics" },
- { to: "/dashboard/messages", icon: MessageSquare, label: "Messages" },
- ].map(({ to, icon: Icon, label }) => (
-
-
-
- {label}
-
-
- ))}
-
-
-
-
-
- {/* Page Header */}
-
-
-
-
-
navigate("/dashboard/collaborations")}
- className="flex items-center gap-2"
- >
-
- Back to Collaborations
-
-
-
{collaboration.collabTitle}
-
Collaboration with {collaboration.collaborator.name}
-
-
-
-
- {collaboration.status}
-
-
-
-
-
-
-
-
-
-
-
- {/* Main Content */}
-
-
-
- Overview
- Messages
- Deliverables
- Timeline
-
- {/* Overview Tab */}
-
-
- {/* AI Project Overview & Recommendations remains inline for now */}
-
-
-
-
- AI Project Overview & Recommendations
-
-
-
-
-
Project Health Analysis
-
-
- This collaboration is progressing well with 65% timeline completion.
- The content creation phase is active and on track.
- Communication frequency is optimal for this stage of the project.
-
-
-
-
-
-
Current Timeline Recommendations
-
-
-
-
- Content Creation Phase: Consider scheduling a review meeting
- within the next 2 days to ensure alignment on video direction and style.
-
-
-
-
-
- Quality Check: Request a preview of the thumbnail design
- to provide early feedback and avoid last-minute revisions.
-
-
-
-
-
- Risk Mitigation: Prepare backup content ideas in case
- the current direction needs adjustment.
-
-
-
-
-
-
-
Communication Tips
-
-
- Pro Tip: Use specific feedback when reviewing content.
- Instead of "make it better," try "increase the energy in the first 30 seconds"
- or "add more close-up shots of the product features."
-
-
-
-
-
-
- {/* Messages Tab */}
-
-
-
- {/* Deliverables Tab */}
-
-
-
-
-
- Deliverables
-
-
-
-
- {mockDeliverables.map((deliverable) => (
-
-
-
-
{deliverable.title}
-
{deliverable.description}
-
-
- {deliverable.status.replace('-', ' ')}
-
-
-
-
-
- Due Date:
- {deliverable.dueDate}
-
-
- Assigned To:
- {deliverable.assignedTo}
-
-
-
- {deliverable.files && deliverable.files.length > 0 && (
-
-
Files:
-
- {deliverable.files.map((file, index) => (
-
-
- {file}
-
- ))}
-
-
- )}
-
-
- handleViewDeliverable(deliverable)}
- >
-
- View
-
-
-
- Edit
-
-
-
- ))}
-
-
-
-
- {/* Timeline Tab */}
-
-
-
-
-
-
- {/* Sidebar */}
-
-
-
-
-
-
-
-
- {/* Deliverable View Modal */}
- {showDeliverableModal && selectedDeliverable && (
-
-
-
-
Deliverable Details
-
-
-
-
-
-
-
- {/* Main Content */}
-
- {/* Deliverable Details */}
-
-
-
- Deliverable Details
-
-
- {selectedDeliverable.title}
-
- {selectedDeliverable.description}
-
-
-
-
-
Status & Progress
-
-
-
- {selectedDeliverable.status.replace('-', ' ')}
-
-
- {selectedDeliverable.status === 'completed' ? '100%' :
- selectedDeliverable.status === 'in-progress' ? '65%' : '0%'} complete
-
-
- {selectedDeliverable.status === 'in-progress' && (
-
- )}
-
-
-
-
Timeline
-
-
- Due Date:
- {selectedDeliverable.dueDate}
-
-
- Assigned To:
- {selectedDeliverable.assignedTo}
-
-
- Created:
- {collaboration.startDate}
-
-
-
-
-
-
-
- {/* Files & Attachments */}
- {selectedDeliverable.files && selectedDeliverable.files.length > 0 && (
-
-
-
-
- Files & Attachments
-
-
-
-
- {selectedDeliverable.files.map((file, index) => (
-
-
-
-
-
-
-
{file}
-
Uploaded 2 days ago
-
-
-
-
-
- Preview
-
-
-
- Download
-
-
-
- ))}
-
-
-
- )}
-
- {/* Comments & Feedback */}
-
-
-
-
- Comments & Feedback
-
-
-
-
-
-
-
-
- {collaboration.collaborator.name.slice(0, 2).toUpperCase()}
-
-
{collaboration.collaborator.name}
-
2 days ago
-
-
- "The first draft is ready for review. I've included the main product features
- and added some B-roll footage. Let me know if you'd like any adjustments to the pacing."
-
-
-
-
-
-
- You
-
-
You
-
1 day ago
-
-
- "Great work! The pacing looks good. Could you add a few more close-up shots
- of the product features around the 1:30 mark?"
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Sidebar */}
-
- {/* Quick Actions */}
-
-
- Quick Actions
-
-
-
-
- Edit Deliverable
-
-
-
- Download All Files
-
-
-
- Send Message
-
- {selectedDeliverable.status !== 'completed' && (
-
-
- Mark Complete
-
- )}
-
-
-
- {/* Deliverable Stats */}
-
-
- Deliverable Stats
-
-
-
-
-
- {selectedDeliverable.status === 'completed' ? '100%' :
- selectedDeliverable.status === 'in-progress' ? '65%' : '0%'}
-
-
Progress
-
-
-
- {selectedDeliverable.files?.length || 0}
-
-
Files
-
-
-
-
-
-
-
- Days Remaining:
- 3 days
-
-
- Comments:
- 2
-
-
- Last Updated:
- 1 day ago
-
-
-
-
-
- {/* Version History */}
-
-
- Version History
-
-
-
-
-
-
-
v1.2 - Final Draft
-
Updated 1 day ago
-
-
-
-
-
-
v1.1 - First Review
-
Updated 2 days ago
-
-
-
-
-
-
v1.0 - Initial Draft
-
Created 3 days ago
-
-
-
-
-
-
-
-
-
-
- )}
-
- {/* Contract Modal */}
- {showContractModal && (
-
-
-
-
- Collaboration Contract - {collaboration.collabTitle}
-
- setShowContractModal(false)}
- className="h-8 w-8 p-0"
- >
-
-
-
-
-
-
-
-
- Contract uploaded on {collaboration.startDate}
-
-
-
-
- Download
-
-
-
- Sign Contract
-
-
-
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/pages/Collaborations.tsx b/Frontend/src/pages/Collaborations.tsx
deleted file mode 100644
index dbbbbc8..0000000
--- a/Frontend/src/pages/Collaborations.tsx
+++ /dev/null
@@ -1,252 +0,0 @@
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card"
-import { ModeToggle } from "../components/mode-toggle"
-import { UserNav } from "../components/user-nav"
-import { Button } from "../components/ui/button"
-import { Input } from "../components/ui/input"
-import { BarChart3, Briefcase, FileText, LayoutDashboard, MessageSquare, Rocket, Search, Users } from "lucide-react"
-import {Link} from "react-router-dom"
-import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar"
-import { Badge } from "../components/ui/badge"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs"
-import { Label } from "../components/ui/label"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select"
-import CreatorMatchGrid from "../components/collaboration-hub/CreatorMatchGrid";
-import { mockCreatorMatches } from "../components/dashboard/creator-collaborations";
-import ActiveCollabsGrid from "../components/collaboration-hub/ActiveCollabsGrid";
-import React from "react";
-import CollabRequests from "../components/collaboration-hub/CollabRequests";
-import { useCollaborationState } from "../hooks/useCollaborationState";
-import { mockCollabIdeas, mockRequestTexts } from "../components/collaboration-hub/mockProfileData";
-import NewCollaborationModal from "../components/collaboration-hub/NewCollaborationModal";
-import CreatorSearchModal from "../components/collaboration-hub/CreatorSearchModal";
-
-export default function CollaborationsPage({ showHeader = true }: { showHeader?: boolean }) {
- const {
- modals,
- filters,
- openNewCollaborationModal,
- closeNewCollaborationModal,
- openAiSearchModal,
- closeAiSearchModal,
- updateFilter,
- resetFilters,
- hasActiveFilters,
- activeFiltersCount,
- } = useCollaborationState();
-
- const handleNewCollabSubmit = (data: any) => {
- console.log("New collaboration request submitted:", data);
- // Handle the submission logic here
- };
-
- const handleCreatorConnect = (creator: any) => {
- console.log("Connecting with creator:", creator);
- // Handle the connection logic here
- };
- return (
-
- {showHeader && (
-
-
-
-
-
Inpact
-
-
- {[
- { to: "/dashboard", icon: LayoutDashboard, label: "Dashboard" },
- { to: "/dashboard/sponsorships", icon: Briefcase, label: "Sponsorships" },
- { to: "/dashboard/collaborations", icon: Users, label: "Collaborations" },
- { to: "/dashboard/contracts", icon: FileText, label: "Contracts" },
- { to: "/dashboard/analytics", icon: BarChart3, label: "Analytics" },
- { to: "/dashboard/messages", icon: MessageSquare, label: "Messages" },
- ].map(({ to, icon: Icon, label }) => (
-
-
-
- {label}
-
-
- ))}
-
-
-
-
- )}
-
-
- {/* Filter Sidebar */}
-
-
- Filters
- Find your ideal collaborators
-
-
-
- Content Niche
- updateFilter('niche', value)}>
-
-
-
-
- All Niches
- Fashion
- Technology
- Beauty
- Fitness
- Food
- Travel
-
-
-
-
-
- Audience Size
- updateFilter('audienceSize', value)}>
-
-
-
-
- All Sizes
- Micro (10K-50K)
- Mid-tier (50K-500K)
- Macro (500K-1M)
- Mega (1M+)
-
-
-
-
-
- Collaboration Type
- updateFilter('collaborationType', value)}>
-
-
-
-
- All Types
- Guest Appearances
- Joint Content
- Challenges
- Content Series
-
-
-
-
-
- Location
- updateFilter('location', value)}>
-
-
-
-
- Anywhere
- United States
- Europe
- Asia
- Remote Only
-
-
-
-
-
-
- Reset Filters
-
-
- Apply Filters
-
-
- {hasActiveFilters && (
-
- {activeFiltersCount} filter{activeFiltersCount !== 1 ? 's' : ''} active
-
- )}
-
-
- {/* Main Content */}
-
- {/* Tabs for AI Matches, Active Collabs, Requests */}
-
-
- AI Matches
- Active Collabs
- Requests
-
-
- {/* Banner */}
-
-
-
- ⚡
- AI-Powered Creator Matching
-
-
Our AI analyzes your content style, audience demographics, and engagement patterns to find your ideal collaborators.
-
-
Refresh Matches
-
- {/* Creator Match Grid with Pagination */}
-
-
-
- {/* View More Recommendations Button */}
-
- View More Recommendations
-
-
-
-
-
-
-
-
- + New Collaboration Request
-
-
- Find Creators with AI
-
-
-
-
- {/* New Collaboration Modal */}
-
-
- {/* AI Creator Search Modal */}
-
-
-
-
-
-
-
- )
-}
-
diff --git a/Frontend/src/pages/Contracts.tsx b/Frontend/src/pages/Contracts.tsx
deleted file mode 100644
index 792b38a..0000000
--- a/Frontend/src/pages/Contracts.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react'
-
-function Contracts() {
- return (
- Contracts
- )
-}
-
-export default Contracts
\ No newline at end of file
diff --git a/Frontend/src/pages/DashboardPage.tsx b/Frontend/src/pages/DashboardPage.tsx
deleted file mode 100644
index e5a8fc2..0000000
--- a/Frontend/src/pages/DashboardPage.tsx
+++ /dev/null
@@ -1,237 +0,0 @@
-import { Link } from "react-router-dom"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs"
-import { ModeToggle } from "../components/mode-toggle"
-import { UserNav } from "../components/user-nav"
-import { Button } from "../components/ui/button"
-import { Input } from "../components/ui/input"
-import {
- BarChart3,
- Briefcase,
- DollarSign,
- FileText,
- Icon,
- LayoutDashboard,
- LogOut,
- MessageSquare,
- Rocket,
- Search,
- Users,
-} from "lucide-react"
-import { PerformanceMetrics } from "../components/dashboard/performance-metrics"
-import { RecentActivity } from "../components/dashboard/recent-activity"
-import { SponsorshipMatches } from "../components/dashboard/sponsorship-matches"
-import { useAuth } from "../context/AuthContext"
-import CollaborationsPage from "./Collaborations";
-
-export default function DashboardPage() {
- const {logout, user} = useAuth();
-
- return (
-
-
-
-
-
-
Inpact
-
-
- {[
- { to: "/dashboard", icon: LayoutDashboard, label: "Dashboard" },
- { to: "/dashboard/sponsorships", icon: Briefcase, label: "Sponsorships" },
- { to: "/dashboard/collaborations", icon: Users, label: "Collaborations" },
- { to: "/dashboard/contracts", icon: FileText, label: "Contracts" },
- { to: "/dashboard/analytics", icon: BarChart3, label: "Analytics" },
- { to: "/dashboard/messages", icon: MessageSquare, label: "Messages" },
- ].map(({ to, icon: Icon, label }) => (
-
-
-
- {label}
-
-
- ))}
-
-
-
-
-
-
-
Dashboard
-
-
-
- New Campaign
-
-
-
-
-
-
- Overview
-
-
- Sponsorships
-
-
- Collaborations
-
-
- Analytics
-
-
-
-
-
-
- Total Revenue
-
-
-
- $45,231.89
- +20.1% from last month
-
-
-
-
- Active Sponsorships
-
-
-
- 12
- +3 from last month
-
-
-
-
- Collaborations
-
-
-
- 8
- +2 from last month
-
-
-
-
- Audience Growth
-
-
-
- +12.5%
- +2.1% from last month
-
-
-
-
-
-
- Performance Metrics
-
-
-
-
-
-
-
- Recent Activity
- Your latest interactions and updates
-
-
-
-
-
-
-
-
-
- AI-Matched Sponsorships
- Brands that match your audience and content
-
-
-
-
-
-
-
- Creator Collaborations
- Creators with complementary audiences
-
-
-
-
-
-
-
-
-
-
- AI-Driven Sponsorship Matchmaking
- Discover brands that align with your audience and content style
-
-
-
-
Coming Soon
-
- The full sponsorship matchmaking interface will be available here.
-
-
-
-
-
-
-
-
-
-
-
- Performance Analytics & ROI Tracking
- Track sponsorship performance and campaign success
-
-
-
-
Coming Soon
-
- The full analytics dashboard will be available here.
-
-
-
-
-
-
-
-
- )
- }
\ No newline at end of file
diff --git a/Frontend/src/pages/ForgotPassword.tsx b/Frontend/src/pages/ForgotPassword.tsx
deleted file mode 100644
index f2d1a03..0000000
--- a/Frontend/src/pages/ForgotPassword.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import { useState } from "react";
-import { Link } from "react-router-dom";
-import { ArrowLeft, Check, Rocket } from "lucide-react";
-import { supabase } from "../utils/supabase";
-
-export default function ForgotPasswordPage() {
- const [email, setEmail] = useState("");
- const [isLoading, setIsLoading] = useState(false);
- const [isSubmitted, setIsSubmitted] = useState(false);
- const [error, setError] = useState("");
- const [showSignupPrompt, setShowSignupPrompt] = useState(false);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setIsLoading(true);
- setError("");
- setShowSignupPrompt(false);
-
- try {
- // Check if the email exists in the users table before sending a reset link
- const { data: users, error: userError } = await supabase
- .from("users")
- .select("id")
- .eq("email", email)
- .maybeSingle();
- if (userError) throw userError;
- if (!users) {
- // If the email does not exist, prompt the user to sign up
- setShowSignupPrompt(true);
- setIsLoading(false);
- return;
- }
- // Send the password reset email using Supabase Auth
- const { error } = await supabase.auth.resetPasswordForEmail(email, {
- redirectTo: window.location.origin + "/reset-password"
- });
- if (error) throw error;
- setIsSubmitted(true);
- } catch (err: any) {
-
- setError(err.message || "Something went wrong. Please try again.");
- } finally {
- setIsLoading(false);
- }
- };
-
- return (
-
-
-
-
-
- Inpact
-
-
-
-
-
-
-
-
-
-
- Back to login
-
-
- {isSubmitted ? (
-
-
-
-
-
- Check your email
-
-
- We've sent a password reset link to{" "}
- {email}
-
-
- Didn't receive the email? Check your spam folder or{" "}
- setIsSubmitted(false)}
- className="text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-300 transition-colors duration-200"
- >
- try another email address
-
-
-
- ) : (
- <>
-
- Reset your password
-
-
- Enter your email address and we'll send you a link to reset
- your password
-
-
- {error && (
-
- {error}
-
- )}
-
- {showSignupPrompt && (
-
- No account found with this email. Sign up?
-
- )}
-
-
-
-
- Email address
-
- setEmail(e.target.value)}
- required
- className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200"
- // Email is case sensitive for password reset
- placeholder="you@example.com (CASE sensitive)"
- />
-
-
-
- {isLoading ? (
-
-
-
-
-
- Sending reset link...
-
- ) : (
- "Send reset link"
- )}
-
-
- >
- )}
-
-
-
-
-
-
- © 2024 Inpact. All rights reserved.
-
-
- );
-}
diff --git a/Frontend/src/pages/HomePage.tsx b/Frontend/src/pages/HomePage.tsx
deleted file mode 100644
index 011641d..0000000
--- a/Frontend/src/pages/HomePage.tsx
+++ /dev/null
@@ -1,937 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { Link } from "react-router-dom";
-import {
- ArrowRight,
- BarChart3,
- Handshake,
- Layers,
- MessageSquare,
- Rocket,
- Users,
- Plus,
- TrendingUp,
- Calendar,
- Star,
- Target,
- Zap,
- BookOpen,
- Award,
- TrendingDown,
- Eye,
- Heart,
- Share2,
- Play,
- UserPlus,
- Sparkles,
-} from "lucide-react";
-import { Button } from "../components/ui/button";
-import { MainNav } from "../components/main-nav";
-import { ModeToggle } from "../components/mode-toggle";
-import { UserNav } from "../components/user-nav";
-import { useAuth } from "../context/AuthContext";
-import { supabase } from "../utils/supabase";
-
-const features = [
- {
- icon: Handshake,
- title: "AI-Driven Matchmaking",
- desc: "Connect with brands based on audience demographics, engagement rates, and content style.",
- gradient: "from-blue-500 to-purple-600",
- },
- {
- icon: Users,
- title: "Creator Collaboration Hub",
- desc: "Find and partner with creators who have complementary audiences and content niches.",
- gradient: "from-green-500 to-blue-600",
- },
- {
- icon: Layers,
- title: "Smart Pricing Optimization",
- desc: "Get fair sponsorship pricing recommendations based on engagement and market trends.",
- gradient: "from-purple-500 to-pink-600",
- },
- {
- icon: MessageSquare,
- title: "AI Contract Assistant",
- desc: "Structure deals, generate contracts, and optimize terms using AI insights.",
- gradient: "from-orange-500 to-red-600",
- },
- {
- icon: BarChart3,
- title: "Performance Analytics",
- desc: "Track sponsorship performance, audience engagement, and campaign success.",
- gradient: "from-indigo-500 to-purple-600",
- },
- {
- icon: Rocket,
- title: "ROI Tracking",
- desc: "Measure and optimize return on investment for both creators and brands.",
- gradient: "from-teal-500 to-green-600",
- },
-];
-
-const dashboardFeatures = [
- {
- icon: TrendingUp,
- title: "Analytics Dashboard",
- desc: "Track your performance metrics, engagement rates, and growth trends.",
- },
- {
- icon: Handshake,
- title: "Active Collaborations",
- desc: "Manage your ongoing partnerships and track collaboration progress.",
- },
- {
- icon: Calendar,
- title: "Campaign Calendar",
- desc: "Schedule and organize your content campaigns and brand partnerships.",
- },
- {
- icon: MessageSquare,
- title: "Communication Hub",
- desc: "Connect with brands and creators through our integrated messaging system.",
- },
- {
- icon: BarChart3,
- title: "Performance Insights",
- desc: "Get detailed analytics and insights to optimize your content strategy.",
- },
- {
- icon: Plus,
- title: "Create New Campaign",
- desc: "Start new collaborations and campaigns with our AI-powered matching system.",
- },
-];
-
-const successStories = [
- {
- creator: "Sarah Chen",
- niche: "Tech & Lifestyle",
- followers: "2.1M",
- brand: "TechFlow",
- result: "500% ROI increase",
- story: "Sarah's authentic tech reviews helped TechFlow launch their new smartphone with record-breaking pre-orders.",
- avatar: "/avatars/sarah.jpg",
- platform: "YouTube",
- },
- {
- creator: "Marcus Rodriguez",
- niche: "Fitness & Wellness",
- followers: "850K",
- brand: "FitFuel",
- result: "300% engagement boost",
- story: "Marcus's workout challenges with FitFuel products generated over 10M views and 50K+ app downloads.",
- avatar: "/avatars/marcus.jpg",
- platform: "Instagram",
- },
- {
- creator: "Emma Thompson",
- niche: "Sustainable Fashion",
- followers: "1.2M",
- brand: "EcoStyle",
- result: "200% sales increase",
- story: "Emma's sustainable fashion content helped EcoStyle become the top eco-friendly brand in their category.",
- avatar: "/avatars/emma.jpg",
- platform: "TikTok",
- },
-];
-
-const trendingNiches = [
- {
- name: "AI & Tech",
- growth: "+45%",
- creators: "12.5K",
- avgEngagement: "8.2%",
- icon: Zap,
- color: "from-blue-500 to-purple-600",
- },
- {
- name: "Sustainable Living",
- growth: "+38%",
- creators: "8.9K",
- avgEngagement: "9.1%",
- icon: Target,
- color: "from-green-500 to-teal-600",
- },
- {
- name: "Mental Health",
- growth: "+52%",
- creators: "15.2K",
- avgEngagement: "7.8%",
- icon: Heart,
- color: "from-pink-500 to-rose-600",
- },
- {
- name: "Gaming & Esports",
- growth: "+41%",
- creators: "22.1K",
- avgEngagement: "6.9%",
- icon: Play,
- color: "from-purple-500 to-indigo-600",
- },
- {
- name: "Personal Finance",
- growth: "+33%",
- creators: "6.8K",
- avgEngagement: "8.5%",
- icon: TrendingUp,
- color: "from-emerald-500 to-green-600",
- },
- {
- name: "Remote Work",
- growth: "+29%",
- creators: "9.3K",
- avgEngagement: "7.2%",
- icon: Users,
- color: "from-orange-500 to-red-600",
- },
-];
-
-const creatorResources = [
- {
- title: "Creator Economy Report 2024",
- desc: "Latest trends, platform changes, and monetization strategies",
- readTime: "8 min read",
- category: "Research",
- icon: BookOpen,
- },
- {
- title: "How to Negotiate Brand Deals",
- desc: "Master the art of pricing and contract negotiation",
- readTime: "12 min read",
- category: "Guide",
- icon: Handshake,
- },
- {
- title: "Content Calendar Templates",
- desc: "Free templates to organize your content strategy",
- readTime: "5 min read",
- category: "Template",
- icon: Calendar,
- },
- {
- title: "Platform Algorithm Updates",
- desc: "Stay ahead with latest social media changes",
- readTime: "6 min read",
- category: "News",
- icon: TrendingUp,
- },
-];
-
-const brandShowcase = [
- {
- name: "TechFlow",
- industry: "Technology",
- logo: "/brands/techflow.png",
- description: "Leading smartphone manufacturer seeking tech reviewers and lifestyle creators",
- followers: "2.5M",
- budget: "$5K - $50K",
- lookingFor: ["Tech Reviewers", "Lifestyle Creators", "Gaming Streamers"],
- activeCampaigns: 3,
- },
- {
- name: "FitFuel",
- industry: "Health & Fitness",
- logo: "/brands/fitfuel.png",
- description: "Premium fitness supplement brand looking for authentic fitness influencers",
- followers: "1.8M",
- budget: "$3K - $25K",
- lookingFor: ["Fitness Trainers", "Nutrition Experts", "Wellness Coaches"],
- activeCampaigns: 5,
- },
- {
- name: "EcoStyle",
- industry: "Sustainable Fashion",
- logo: "/brands/ecostyle.png",
- description: "Eco-friendly fashion brand seeking sustainable lifestyle advocates",
- followers: "950K",
- budget: "$2K - $20K",
- lookingFor: ["Fashion Influencers", "Sustainability Advocates", "Lifestyle Creators"],
- activeCampaigns: 2,
- },
- {
- name: "GameZone",
- industry: "Gaming",
- logo: "/brands/gamezone.png",
- description: "Gaming accessories company looking for esports and gaming content creators",
- followers: "3.2M",
- budget: "$4K - $40K",
- lookingFor: ["Gaming Streamers", "Esports Players", "Tech Reviewers"],
- activeCampaigns: 4,
- },
-];
-
-// TrendingNichesSection: Fetches and displays trending niches from the backend
-function TrendingNichesSection() {
- // State for trending niches, loading, and error
- const [niches, setNiches] = useState<{ name: string; insight: string; global_activity: number }[]>([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- // Fetch trending niches from the backend API on mount
- useEffect(() => {
- fetch("/api/trending-niches")
- .then(res => {
- if (!res.ok) throw new Error("Failed to fetch trending niches");
- return res.json();
- })
- .then(data => {
- setNiches(data);
- setLoading(false);
- })
- .catch(err => {
- setError(err.message);
- setLoading(false);
- });
- }, []);
-
- if (loading) return Loading trending niches...
;
- if (error) return Error: {error}
;
-
- // Emoji icons for visual variety in cards
- const icons = ['🤖','🌱','🎮','💸','✈️','🧩'];
-
- // Modern glassmorphism card design for each trending niche
- return (
-
- {niches.map((niche, idx) => (
-
- {/* Gradient overlay for extra glass effect */}
-
- {/* Floating Emoji icon above the card */}
-
- {icons[idx % icons.length]}
-
- {/* Niche name */}
-
{niche.name}
- {/* Niche insight as a quote */}
-
- “{niche.insight}”
-
- {/* Global activity as a progress bar */}
-
-
Global Activity
-
-
-
= 4
- ? 'bg-gradient-to-r from-purple-500 to-blue-500'
- : 'bg-gradient-to-r from-yellow-400 to-orange-500'
- }`}
- style={{ width: `${(niche.global_activity / 5) * 100}%` }}
- />
-
-
{niche.global_activity}/5
-
-
-
- ))}
-
- );
-}
-
-function WhyChooseSection() {
- return (
-
-
-
Why Choose Inpact AI?
-
- Powerful tools for both brands and creators to connect, collaborate, and grow.
-
-
- {/* Brands Column */}
-
-
-
- For Brands
-
-
- AI-driven creator matching for your campaigns
- Real-time performance analytics & ROI tracking
- Smart pricing & budget optimization
- Streamlined communication & contract management
-
-
- {/* Creators Column */}
-
-
-
- For Creators
-
-
- Get discovered by top brands in your niche
- Fair sponsorship deals & transparent payments
- AI-powered content & contract assistant
- Grow your audience & track your impact
-
-
-
-
-
- );
-}
-
-export default function HomePage() {
- const { isAuthenticated, user } = useAuth();
-
- // Refs for scroll detection
- const featuresRef = useRef(null);
- const successStoriesRef = useRef(null);
- const trendingRef = useRef(null);
- const resourcesRef = useRef(null);
- const footerRef = useRef(null);
-
- // State to track visibility (for one-time animation)
- const [isFeaturesVisible, setIsFeaturesVisible] = useState(false);
- const [isSuccessStoriesVisible, setIsSuccessStoriesVisible] = useState(false);
- const [isTrendingVisible, setIsTrendingVisible] = useState(false);
- const [isResourcesVisible, setIsResourcesVisible] = useState(false);
- const [isFooterVisible, setIsFooterVisible] = useState(false);
-
- // One-time animation state
- const [hasAnimatedTrending, setHasAnimatedTrending] = useState(false);
- const [hasAnimatedBrands, setHasAnimatedBrands] = useState(false);
-
- // Set up intersection observer for scroll detection (one-time animation)
- useEffect(() => {
- const trendingObserver = new IntersectionObserver(
- (entries) => {
- const [entry] = entries;
- if (entry.isIntersecting && !hasAnimatedTrending) {
- setIsTrendingVisible(true);
- setHasAnimatedTrending(true);
- }
- },
- { root: null, rootMargin: "0px", threshold: 0.1 }
- );
- const brandsObserver = new IntersectionObserver(
- (entries) => {
- const [entry] = entries;
- if (entry.isIntersecting && !hasAnimatedBrands) {
- setIsSuccessStoriesVisible(true);
- setHasAnimatedBrands(true);
- }
- },
- { root: null, rootMargin: "0px", threshold: 0.1 }
- );
- if (trendingRef.current) trendingObserver.observe(trendingRef.current);
- if (successStoriesRef.current) brandsObserver.observe(successStoriesRef.current);
- return () => {
- if (trendingRef.current) trendingObserver.unobserve(trendingRef.current);
- if (successStoriesRef.current) brandsObserver.unobserve(successStoriesRef.current);
- };
- }, [hasAnimatedTrending, hasAnimatedBrands]);
-
- // ... keep other observers for footer, etc. if needed ...
- useEffect(() => {
- const footerObserver = new IntersectionObserver(
- (entries) => {
- const [entry] = entries;
- setIsFooterVisible(entry.isIntersecting);
- },
- { root: null, rootMargin: "0px", threshold: 0.1 }
- );
- if (footerRef.current) footerObserver.observe(footerRef.current);
- return () => {
- if (footerRef.current) footerObserver.unobserve(footerRef.current);
- };
- }, []);
-
- // Logged-in user homepage
- if (isAuthenticated && user) {
- return (
-
- {/* Header with glassmorphism */}
-
-
- {/* Hero Section - Image Left, Text Right, Text More Centered */}
-
-
- {/* Background elements */}
-
-
-
-
-
-
-
- {/* Left Image */}
-
-
- {/* 3D Glow Effect */}
-
-
- {/* Main Image */}
-
-
-
- {/* Floating Elements */}
-
-
-
-
-
-
-
-
-
-
-
- {/* Right Content */}
-
-
-
- {/* Main Welcome Heading */}
-
- Welcome, {user.user_metadata?.name || user.email?.split('@')[0]}
-
-
- Ready to grow your creator business? Explore new opportunities, track your performance, and connect with brands.
-
-
- {/* Action Buttons */}
-
-
-
- Go to Dashboard
-
-
-
-
- Browse Opportunities
-
-
-
- {/* How It Works Row */}
-
-
-
- Create your profile
-
-
-
- Get matched by AI
-
-
-
- Collaborate & grow
-
-
-
-
-
-
-
- {/* Why Choose Inpact AI Section (for logged out users) */}
-
-
- {/* Trending Niches Section - Centered Grid, No Extra Right Space */}
-
-
-
-
- Trending Niches
-
-
- Discover the fastest-growing content categories and opportunities.
-
-
-
-
-
- {/* Brand Showcase Section - Centered Grid, No Extra Right Space */}
-
-
-
-
- Brands Seeking Creators
-
-
- Connect with companies actively looking for creators like you.
-
-
- {brandShowcase.map((brand, idx) => (
-
-
-
-
-
- {brand.name.split('').slice(0, 2).join('')}
-
-
-
-
-
-
{brand.name}
-
{brand.industry}
-
{brand.description}
-
-
-
-
-
Followers
-
{brand.followers}
-
-
-
Budget Range
-
{brand.budget}
-
-
-
Active Campaigns
-
{brand.activeCampaigns}
-
-
-
Looking For
-
{brand.lookingFor.length} types
-
-
-
- {brand.lookingFor.map((type, typeIdx) => (
-
- {type}
-
- ))}
-
-
-
- View Opportunities
-
-
-
- ))}
-
-
-
-
- {/* Footer */}
-
-
-
- );
- }
-
- // Non-logged-in user homepage (redesigned)
- return (
-
- {/* Header with glassmorphism */}
-
-
-
-
-
-
-
- Login
-
-
- Sign Up
-
-
-
-
-
-
-
- {/* Hero Section - Completely Redesigned */}
-
-
- {/* Background elements */}
-
-
-
-
-
-
-
- {/* Left Content */}
-
-
-
-
AI-Powered Platform
-
-
-
- {/* 3D Text Effect for "Inpact AI" */}
-
-
- INPACT AI
-
-
- Creator Collaboration Platform
-
-
-
-
- Connect with brands, collaborate with creators, and optimize your partnerships through data-driven insights.
-
-
-
-
-
-
- Get Started
-
-
-
- Learn More
-
-
- {/* How It Works Row */}
-
-
-
- Create your profile
-
-
-
- Get matched by AI
-
-
-
- Collaborate & grow
-
-
-
-
- {/* Right Image */}
-
-
- {/* 3D Glow Effect */}
-
-
-
- {/* Main Image */}
-
-
-
-
- {/* Floating Elements */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Why Choose Inpact AI Section (for logged out users) */}
-
-
- {/* Success Stories Section */}
-
-
-
-
- Success Stories
-
-
- Real creators achieving amazing results with brand partnerships.
-
-
- {successStories.map((story, idx) => (
-
-
-
-
-
- {story.creator.split(' ').map(n => n[0]).join('')}
-
-
-
-
-
-
{story.creator}
-
{story.niche}
-
-
-
-
{story.story}
-
-
-
- {story.followers}
-
-
-
- {story.platform}
-
-
-
-
-
- Result: {story.result}
-
-
-
- ))}
-
-
-
-
- {/* Trending Niches Section */}
-
-
-
-
- Trending Niches
-
-
- Discover the fastest-growing content categories and opportunities.
-
-
-
-
-
- {/* Footer */}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/pages/Login.tsx b/Frontend/src/pages/Login.tsx
deleted file mode 100644
index 875567a..0000000
--- a/Frontend/src/pages/Login.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-import { useState } from "react";
-import { Link, useNavigate } from "react-router-dom";
-import { Eye, EyeOff, Rocket } from "lucide-react";
-import { supabase } from "../utils/supabase";
-import { useAuth } from "../context/AuthContext";
-
-export default function LoginPage() {
- const Navigate = useNavigate();
- const { login } = useAuth();
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const [showPassword, setShowPassword] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState("");
-
- const handleSubmit = async (e: React.FormEvent
) => {
- e.preventDefault();
- setIsLoading(true);
- setError("");
-
- try {
- const { data, error } = await supabase.auth.signInWithPassword({
- email,
- password,
- });
-
- if (error) {
- setError(error.message);
- setIsLoading(false);
- return;
- }
-
- // AuthContext will handle navigation based on user onboarding status and role
- setIsLoading(false);
- } catch (err) {
- setError("Invalid email or password. Please try again.");
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleGoogleLogin = async () => {
- const { data, error } = await supabase.auth.signInWithOAuth({
- provider: "google",
- });
-
- if (error) {
- console.log("Google login error", error);
- return;
- }
-
- // AuthContext will handle navigation based on user onboarding status and role
- };
-
- return (
-
-
-
-
-
- Inpact
-
-
-
-
- Don't have an account?
-
-
- Sign up
-
-
-
-
-
-
-
-
-
- Welcome back
-
-
- Sign in to your account to continue
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
-
- Email
-
- setEmail(e.target.value)}
- required
- className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200"
- placeholder="you@example.com"
- />
-
-
-
-
-
- Password
-
-
- Forgot password?
-
-
-
- setPassword(e.target.value)}
- required
- className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200"
- placeholder="••••••••"
- />
- setShowPassword(!showPassword)}
- className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors duration-200"
- >
- {showPassword ? (
-
- ) : (
-
- )}
-
-
-
-
-
- {isLoading ? (
-
-
-
-
-
- Signing in...
-
- ) : (
- "Sign in"
- )}
-
-
-
-
-
-
-
-
- Or continue with
-
-
-
-
-
-
handleGoogleLogin()}
- className="w-full inline-flex justify-center py-3 px-4 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm bg-white dark:bg-gray-700 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors duration-200"
- >
-
-
-
- Google
-
-
-
-
-
- Facebook
-
-
-
-
-
-
-
-
-
- © 2024 Inpact. All rights reserved.
-
-
- );
-}
diff --git a/Frontend/src/pages/Messages.tsx b/Frontend/src/pages/Messages.tsx
deleted file mode 100644
index 332b854..0000000
--- a/Frontend/src/pages/Messages.tsx
+++ /dev/null
@@ -1,402 +0,0 @@
-import { useState } from "react";
-import { Button } from "../components/ui/button";
-import { Input } from "../components/ui/input";
-import {
- BarChart3,
- Briefcase,
- FileText,
- LayoutDashboard,
- MessageSquare,
- Rocket,
- Search,
- Send,
- Users,
-} from "lucide-react";
-import { Link } from "react-router-dom";
-import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar";
-import { Badge } from "../components/ui/badge";
-import { ModeToggle } from "../components/mode-toggle";
-import { UserNav } from "../components/user-nav";
-import { ScrollArea } from "../components/ui/scroll-area";
-import { Separator } from "../components/ui/separator";
-import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
-import Chat from "@/components/chat/chat";
-
-const contacts = [
- {
- id: "1",
- name: "EcoStyle",
- avatar: "/placeholder.svg?height=40&width=40",
- initials: "ES",
- lastMessage:
- "Let's discuss the contract details for our upcoming campaign.",
- time: "10:30 AM",
- unread: true,
- type: "brand",
- },
- {
- id: "2",
- name: "Sarah Johnson",
- avatar: "/placeholder.svg?height=40&width=40",
- initials: "SJ",
- lastMessage: "I'm excited about our travel vlog collaboration!",
- time: "Yesterday",
- unread: true,
- type: "creator",
- },
- {
- id: "3",
- name: "TechGadgets",
- avatar: "/placeholder.svg?height=40&width=40",
- initials: "TG",
- lastMessage:
- "We've shipped the products for your review. You should receive them by tomorrow.",
- time: "Yesterday",
- unread: false,
- type: "brand",
- },
- {
- id: "4",
- name: "Mike Chen",
- avatar: "/placeholder.svg?height=40&width=40",
- initials: "MC",
- lastMessage: "I've shared the draft script for our tech comparison video.",
- time: "2 days ago",
- unread: false,
- type: "creator",
- },
- {
- id: "5",
- name: "FitLife Supplements",
- avatar: "/placeholder.svg?height=40&width=40",
- initials: "FL",
- lastMessage: "Would you be interested in a 3-month partnership?",
- time: "3 days ago",
- unread: false,
- type: "brand",
- },
- {
- id: "6",
- name: "Leila Ahmed",
- avatar: "/placeholder.svg?height=40&width=40",
- initials: "LA",
- lastMessage: "Let's schedule our fashion lookbook shoot for next week.",
- time: "1 week ago",
- unread: false,
- type: "creator",
- },
-];
-
-const messages = [
- {
- id: "1",
- sender: "user",
- content: "Hi EcoStyle team! I'm excited about our potential partnership.",
- time: "10:15 AM",
- },
- {
- id: "2",
- sender: "contact",
- content:
- "Hello! We're thrilled to work with you too. We love your content and think you'd be a perfect fit for our sustainable fashion line.",
- time: "10:18 AM",
- },
- {
- id: "3",
- sender: "user",
- content:
- "That's great to hear! I've been a fan of your brand for a while and appreciate your commitment to sustainability.",
- time: "10:20 AM",
- },
- {
- id: "4",
- sender: "contact",
- content:
- "Thank you! We've been following your content and your audience aligns perfectly with our target demographic. We'd like to discuss a potential sponsorship for our new summer collection.",
- time: "10:22 AM",
- },
- {
- id: "5",
- sender: "user",
- content:
- "I'd be very interested in that. What kind of deliverables are you looking for?",
- time: "10:25 AM",
- },
- {
- id: "6",
- sender: "contact",
- content:
- "We're thinking about 1 dedicated post and 2 stories highlighting our sustainable materials and production process. We'd also love if you could share your authentic experience with our products.",
- time: "10:28 AM",
- },
- {
- id: "7",
- sender: "contact",
- content:
- "Let's discuss the contract details for our upcoming campaign. We can use Inpact's AI contract generator to streamline the process.",
- time: "10:30 AM",
- },
-];
-
-export default function MessagesPage() {
- const [activeContact, setActiveContact] = useState(contacts[0]);
- const [messageInput, setMessageInput] = useState("");
- const [activeMessages, setActiveMessages] = useState(messages);
- const [activeTab, setActiveTab] = useState("all");
-
- const handleSendMessage = () => {
- if (messageInput.trim() === "") return;
-
- const newMessage = {
- id: String(activeMessages.length + 1),
- sender: "user",
- content: messageInput,
- time: new Date().toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- }),
- };
-
- setActiveMessages([...activeMessages, newMessage]);
- setMessageInput("");
- };
-
- const filteredContacts =
- activeTab === "all"
- ? contacts
- : contacts.filter((contact) => contact.type === activeTab);
-
- return (
-
-
-
-
-
-
- Inpact
-
-
-
- {[
- { to: "/dashboard", icon: LayoutDashboard, label: "Dashboard" },
- {
- to: "/dashboard/sponsorships",
- icon: Briefcase,
- label: "Sponsorships",
- },
- {
- to: "/dashboard/collaborations",
- icon: Users,
- label: "Collaborations",
- },
- {
- to: "/dashboard/contracts",
- icon: FileText,
- label: "Contracts",
- },
- {
- to: "/dashboard/analytics",
- icon: BarChart3,
- label: "Analytics",
- },
- {
- to: "/dashboard/messages",
- icon: MessageSquare,
- label: "Messages",
- },
- ].map(({ to, icon: Icon, label }) => (
-
-
-
- {label}
-
-
- ))}
-
-
-
-
- {/* Old Code */}
-
- {/* Sidebar */}
-
- {/* Search Input */}
-
-
- {/* Tabs Section */}
-
-
-
-
- All
-
-
- Brands
-
-
- Creators
-
-
-
-
- {/* Contacts List */}
-
-
- {filteredContacts.map((contact) => (
-
-
setActiveContact(contact)}
- >
-
-
-
- {contact.initials}
-
-
-
-
- {contact.name}
-
-
- {contact.time}
-
-
-
- {contact.lastMessage}
-
-
- {contact.unread && (
-
- )}
-
-
-
-
- ))}
-
-
-
-
-
- {/* Chat Section */}
-
- {/* Chat Header */}
-
-
-
-
- {activeContact.initials}
-
-
-
{activeContact.name}
-
- {activeContact.type === "brand" ? "Brand Partner" : "Creator"}
-
-
-
-
-
- View Profile
-
-
- Create Contract
-
-
-
-
- {/* Messages Area */}
-
-
- {activeMessages.map((message) => (
-
-
-
{message.content}
-
{message.time}
-
-
- ))}
-
-
-
- {/* Message Input */}
-
-
- setMessageInput(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- handleSendMessage();
- }
- }}
- className="w-full p-2 border rounded-md bg-gray-100 dark:bg-gray-700 focus:ring focus:ring-purple-300"
- />
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/Frontend/src/pages/ResetPassword.tsx b/Frontend/src/pages/ResetPassword.tsx
deleted file mode 100644
index 2beff71..0000000
--- a/Frontend/src/pages/ResetPassword.tsx
+++ /dev/null
@@ -1,357 +0,0 @@
-import { useState, useEffect, useRef } from "react";
-import { Link } from "react-router-dom";
-import { useNavigate, useParams } from "react-router-dom";
-import { Check, Eye, EyeOff, Rocket } from "lucide-react";
-import { supabase } from "../utils/supabase";
-
-export default function ResetPasswordPage() {
- const router = useNavigate();
- const searchParams = useParams();
-
- const [password, setPassword] = useState("");
- const [confirmPassword, setConfirmPassword] = useState("");
- const [showPassword, setShowPassword] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState("");
- const [isSuccess, setIsSuccess] = useState(false);
- const [progress, setProgress] = useState(0);
- const progressRef = useRef(null);
-
- useEffect(() => {
- // Supabase will automatically handle the session if the user comes from the reset link
- // No need to manually extract token
- }, []);
-
- useEffect(() => {
- if (isSuccess) {
- setProgress(0);
- progressRef.current = setInterval(() => {
- setProgress((prev) => {
- if (prev >= 100) {
- if (progressRef.current) clearInterval(progressRef.current);
- router("/dashboard");
- return 100;
- }
- return prev + (100 / 30); // 3 seconds, 100ms interval
- });
- }, 100);
- }
- return () => {
- if (progressRef.current) clearInterval(progressRef.current);
- };
- }, [isSuccess, router]);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- if (password !== confirmPassword) {
- setError("Passwords don't match");
- return;
- }
-
- setIsLoading(true);
- setError("");
-
- try {
- // Update the user's password using Supabase Auth
- // Supabase automatically authenticates the user from the reset link
- const { error } = await supabase.auth.updateUser({ password });
- if (error) throw error;
- setIsSuccess(true);
- // After success,redirect to dashboard
- } catch (err: any) {
-
- setError(err.message || "Something went wrong. Please try again.");
- } finally {
- setIsLoading(false);
- }
- };
-
- const passwordStrength = () => {
- if (!password)
- return { strength: 0, text: "", color: "bg-gray-200 dark:bg-gray-700" };
-
- let strength = 0;
- if (password.length >= 8) strength += 1;
- if (/[A-Z]/.test(password)) strength += 1;
- if (/[0-9]/.test(password)) strength += 1;
- if (/[^A-Za-z0-9]/.test(password)) strength += 1;
-
- const strengthMap = [
- { text: "Weak", color: "bg-red-500" },
- { text: "Fair", color: "bg-orange-500" },
- { text: "Good", color: "bg-yellow-500" },
- { text: "Strong", color: "bg-green-500" },
- ];
-
- return {
- strength,
- text: strengthMap[strength - 1]?.text || "",
- color: strengthMap[strength - 1]?.color || "bg-gray-200 dark:bg-gray-700",
- };
- };
-
- const { strength, text, color } = passwordStrength();
-
- if (isSuccess) {
- return (
-
-
-
-
-
- Inpact
-
-
-
-
-
-
-
- Password Changed Successfully
-
-
- Redirecting you to your application...
-
-
-
-
-
-
- © 2024 Inpact. All rights reserved.
-
-
- );
- }
-
- return (
-
-
-
-
-
- Inpact
-
-
-
-
-
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
-
- New Password
-
-
- setPassword(e.target.value)}
- required
- className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200"
- placeholder="••••••••"
- />
- setShowPassword(!showPassword)}
- className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors duration-200"
- >
- {showPassword ? (
-
- ) : (
-
- )}
-
-
-
- {password && (
-
-
-
- Password strength: {text}
-
-
- {strength}/4
-
-
-
-
-
- = 8
- ? "text-green-500"
- : "text-gray-400"
- }`}
- >
- {password.length >= 8 ? (
-
- ) : (
- "○"
- )}
-
- At least 8 characters
-
-
-
- {/[A-Z]/.test(password) ? (
-
- ) : (
- "○"
- )}
-
- At least 1 uppercase letter
-
-
-
- {/[0-9]/.test(password) ? (
-
- ) : (
- "○"
- )}
-
- At least 1 number
-
-
-
- {/[^A-Za-z0-9]/.test(password) ? (
-
- ) : (
- "○"
- )}
-
- At least 1 special character
-
-
-
- )}
-
-
-
-
- Confirm Password
-
-
- setConfirmPassword(e.target.value)}
- required
- className={`w-full px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200 ${
- confirmPassword && password !== confirmPassword
- ? "border-red-500 dark:border-red-500"
- : "border-gray-300 dark:border-gray-600"
- }`}
- placeholder="••••••••"
- />
-
- {confirmPassword && password !== confirmPassword && (
-
- Passwords don't match
-
- )}
-
-
-
- {isLoading ? (
-
-
-
-
-
- Resetting password...
-
- ) : (
- "Reset Password"
- )}
-
-
-
-
-
-
-
-
- © 2024 Inpact. All rights reserved.
-
-
- );
-}
diff --git a/Frontend/src/pages/RoleSelection.tsx b/Frontend/src/pages/RoleSelection.tsx
deleted file mode 100644
index 9905a64..0000000
--- a/Frontend/src/pages/RoleSelection.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useState } from "react";
-import { useNavigate } from "react-router-dom";
-
-export default function RoleSelection() {
- const [selectedRole, setSelectedRole] = useState("");
- const [error, setError] = useState("");
- const navigate = useNavigate();
-
- const handleSelect = (role: string) => {
- setSelectedRole(role);
- setError("");
- };
-
- const handleContinue = () => {
- if (!selectedRole) {
- setError("Please select a role to continue.");
- return;
- }
- if (selectedRole === "brand") {
- navigate("/onboarding/brand");
- } else if (selectedRole === "creator") {
- navigate("/onboarding/creator");
- }
- };
-
- return (
-
-
-
Choose your role
-
Select whether you want to sign up as a Brand or a Creator. You cannot change this later.
- {error && (
-
- {error}
-
- )}
-
- handleSelect("creator")}
- >
- Creator
-
- handleSelect("brand")}
- >
- Brand
-
-
-
- Continue
-
-
-
- );
-}
\ No newline at end of file
diff --git a/Frontend/src/pages/Signup.tsx b/Frontend/src/pages/Signup.tsx
deleted file mode 100644
index 0055239..0000000
--- a/Frontend/src/pages/Signup.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-
-import { useState } from "react";
-import { Link, useNavigate } from "react-router-dom";
-import { Check, Eye, EyeOff, Rocket } from "lucide-react";
-import { supabase } from "../utils/supabase";
-import { useAuth } from "@/context/AuthContext";
-import { demoInsert } from '../utils/demoInsert';
-
-export default function SignupPage() {
- const navigate = useNavigate();
- const [formData, setFormData] = useState({
- name: "",
- email: "",
- password: "",
- confirmPassword: ""
- });
- const [showPassword, setShowPassword] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState("");
- const [step, setStep] = useState(1);
- const [user, setuser] = useState("influencer");
- const { login } = useAuth();
-
- const handleChange = (e: React.ChangeEvent) => {
- const { name, value } = e.target;
- setFormData((prev) => ({ ...prev, [name]: value }));
- };
-
- const handleAccountTypeChange = (type: string) => {
- setuser(type);
- setFormData((prev) => ({ ...prev, accountType: type }));
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (formData.password !== formData.confirmPassword) {
- setError("Passwords do not match");
- return;
- }
- setIsLoading(true);
- setError("");
- try {
- const { name, email, password } = formData;
-
- // Check if user already exists
- const { data: existingUser } = await supabase.auth.signInWithPassword({
- email,
- password: "dummy-password-to-check-existence",
- });
-
- if (existingUser.user) {
- setError("An account with this email already exists. Please sign in instead.");
- setIsLoading(false);
- return;
- }
-
- const { data, error } = await supabase.auth.signUp({
- email,
- password,
- options: { data: { name } },
- });
- if (error) {
- if (error.message.includes("already registered")) {
- setError("An account with this email already exists. Please sign in instead.");
- } else {
- setError(error.message);
- }
- setIsLoading(false);
- return;
- }
- setIsLoading(false);
- // AuthContext will handle navigation based on user onboarding status and role
- } catch (err) {
- setError("Something went wrong. Please try again.");
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleGoogleSignUp = async () => {
- const { data, error } = await supabase.auth.signInWithOAuth({
- provider: "google",
- });
-
- if (error) {
- console.log("Google login error", error);
- return;
- }
-
- // AuthContext will handle navigation based on user onboarding status and role
- };
-
- const passwordStrength = () => {
- const { password } = formData;
- if (!password)
- return { strength: 0, text: "", color: "bg-gray-200 dark:bg-gray-700" };
-
- let strength = 0;
- if (password.length >= 8) strength += 1;
- if (/[A-Z]/.test(password)) strength += 1;
- if (/[0-9]/.test(password)) strength += 1;
- if (/[^A-Za-z0-9]/.test(password)) strength += 1;
-
- const strengthMap = [
- { text: "Weak", color: "bg-red-500" },
- { text: "Fair", color: "bg-orange-500" },
- { text: "Good", color: "bg-yellow-500" },
- { text: "Strong", color: "bg-green-500" },
- ];
-
- return {
- strength,
- text: strengthMap[strength - 1]?.text || "",
- color: strengthMap[strength - 1]?.color || "bg-gray-200 dark:bg-gray-700",
- };
- };
-
- const { strength, text, color } = passwordStrength();
-
- return (
-
-
-
-
-
- Inpact
-
-
-
-
- Already have an account?
-
-
- Sign in
-
-
-
-
-
-
-
-
-
- {step === 1 ? "Create your account" : "Complete your profile"}
-
-
- {step === 1
- ? "Join the AI-powered creator collaboration platform"
- : "How do you want to use our Platform?"}
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
- Email
-
-
-
- Password
-
-
-
- Confirm Password
-
-
- {isLoading ? "Signing up..." : "Sign Up"}
-
-
- {step === 1 && (
-
-
-
-
-
- Or continue with
-
-
-
-
-
-
-
-
-
- Google
-
-
-
-
-
- Facebook
-
-
-
- )}
-
-
-
-
-
-
- © 2024 Inpact. All rights reserved.
-
-
- );
-}
diff --git a/Frontend/src/pages/Sponsorships.tsx b/Frontend/src/pages/Sponsorships.tsx
deleted file mode 100644
index 67e813e..0000000
--- a/Frontend/src/pages/Sponsorships.tsx
+++ /dev/null
@@ -1,360 +0,0 @@
-import React from "react"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card"
-import { ModeToggle } from "../components/mode-toggle"
-import { UserNav } from "../components/user-nav"
-import { Button } from "../components/ui/button"
-import { Input } from "../components/ui/input"
-import {
- BarChart3,
- Briefcase,
- DollarSign,
- FileText,
- LayoutDashboard,
- MessageSquare,
- Rocket,
- Search,
- Users,
-} from "lucide-react"
-import { Link } from "react-router-dom"
-import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar"
-import { Badge } from "../components/ui/badge"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs"
-import { Slider } from "../components/ui/slider"
-import { Label } from "../components/ui/label"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select"
-
-export default function SponsorshipsPage() {
- return (
-
-
-
-
-
AI-Driven Sponsorship Matchmaking
-
-
-
- Create Proposal
-
-
-
-
-
-
-
- Filters
- Refine your sponsorship matches
-
-
-
- Category
-
-
-
-
-
- All Categories
- Fashion
- Technology
- Beauty
- Fitness
- Food
- Travel
-
-
-
-
-
-
Budget Range
-
-
-
-
- $1,000
- $10,000
-
-
-
-
- Campaign Type
-
-
-
-
-
- All Types
- Single Post
- Content Series
- Long-term Partnership
- Affiliate Program
-
-
-
-
-
- Minimum Match Score
-
-
-
-
-
- 90% and above
- 80% and above
- 70% and above
- 60% and above
-
-
-
-
- Apply Filters
-
-
-
-
-
-
- AI Matches
- Active Deals
- History
-
-
-
-
-
-
-
- ES
-
-
-
-
-
EcoStyle
-
Sustainable fashion brand
-
-
98% Match
-
-
- EcoStyle is looking for lifestyle creators who can showcase their sustainable fashion line to
- environmentally conscious audiences. Their products include eco-friendly clothing,
- accessories, and home goods.
-
-
-
-
Budget
-
$3,000 - $5,000
-
-
-
Duration
-
1-2 months
-
-
-
Deliverables
-
1 post, 2 stories
-
-
-
Audience Match
-
Very High
-
-
-
- View Full Details
-
- Contact Brand
-
-
- Generate Proposal
-
-
-
-
-
-
-
-
-
-
-
-
- TG
-
-
-
-
-
TechGadgets
-
Consumer electronics company
-
-
95% Match
-
-
- TechGadgets is seeking tech-savvy creators to review and showcase their new line of smart home
- products. They're looking for in-depth reviews that highlight features and user experience.
-
-
-
-
Budget
-
$2,500 - $4,000
-
-
-
-
Deliverables
-
Review video + posts
-
-
-
Audience Match
-
High
-
-
-
- View Full Details
-
- Contact Brand
-
-
- Generate Proposal
-
-
-
-
-
-
-
-
-
-
-
-
- FL
-
-
-
-
-
FitLife Supplements
-
Health and wellness brand
-
-
92% Match
-
-
- FitLife is looking for health and fitness creators to promote their new line of plant-based
- supplements. They want authentic content showing how their products integrate into a healthy
- lifestyle.
-
-
-
-
Budget
-
$1,800 - $3,500
-
-
-
-
Deliverables
-
Monthly content
-
-
-
Audience Match
-
Very High
-
-
-
- View Full Details
-
- Contact Brand
-
-
- Generate Proposal
-
-
-
-
-
-
-
-
-
-
- Active Sponsorships
- Your current brand partnerships
-
-
- Your active sponsorships will appear here.
-
-
-
-
-
-
- Sponsorship History
- Your past brand partnerships
-
-
- Your sponsorship history will appear here.
-
-
-
-
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/Frontend/src/redux/chatSlice.ts b/Frontend/src/redux/chatSlice.ts
deleted file mode 100644
index bf1677e..0000000
--- a/Frontend/src/redux/chatSlice.ts
+++ /dev/null
@@ -1,257 +0,0 @@
-import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import { NewMessageResponse } from "@/types/chat";
-
-// Define the shape of a message
-export interface Message {
- id: string;
- chatListId: string;
- isSent: boolean;
- createdAt: string;
- message: string;
- status?: "sent" | "delivered" | "seen";
-}
-
-// Define the shape of a receiver
-interface Receiver {
- id: string;
- username?: string;
- profileImage?: string;
- isOnline: boolean;
- lastSeen: string | null;
-}
-
-// Define the shape of a chat
-export interface Chat {
- id: string;
- receiver: Receiver;
- messageIds: string[]; // Array of message IDs
- lastMessageTime: string;
-}
-
-// Define the shape of the chat state
-interface ChatState {
- chats: { [chatListId: string]: Chat }; // Normalized chats
- messages: { [message_id: string]: Message }; // Normalized messages
- selectedChatId: string | null; // Currently selected chat
-}
-
-// Initial state
-const initialState: ChatState = {
- chats: {},
- messages: {},
- selectedChatId: null,
-};
-
-// Create the chat slice
-const chatSlice = createSlice({
- name: "chat",
- initialState,
- reducers: {
- // Add a new chat
- addChat: (
- state,
- action: PayloadAction<{
- chatListId: string;
- lastMessageTime: string;
- receiver: Receiver;
- }>
- ) => {
- const { chatListId, receiver, lastMessageTime } = action.payload;
- if (!state.chats[chatListId]) {
- state.chats[chatListId] = {
- id: chatListId,
- receiver,
- messageIds: [],
- lastMessageTime: new Date(lastMessageTime).toISOString(),
- };
- }
- },
-
- addChats: (
- state,
- action: PayloadAction<
- { chatListId: string; lastMessageTime: string; receiver: Receiver }[]
- >
- ) => {
- action.payload.forEach(({ chatListId, lastMessageTime, receiver }) => {
- if (!state.chats[chatListId]) {
- state.chats[chatListId] = {
- id: chatListId,
- receiver,
- messageIds: [],
- lastMessageTime: new Date(lastMessageTime).toISOString(),
- };
- }
- });
- },
-
- // Add a message to a chat
- addMessage: (
- state,
- action: PayloadAction<{ chatListId: string; message: NewMessageResponse }>
- ) => {
- const { chatListId, message } = action.payload;
-
- const newMessage: Message = {
- id: message.id,
- chatListId: chatListId,
- isSent: message.isSent,
- createdAt: new Date(message.createdAt).toISOString(),
- message: message.message,
- status: message.status,
- };
-
- // Add the message to the normalized messages
- state.messages[newMessage.id] = newMessage;
-
- // Add the message ID to the chat's messageIds array
- if (state.chats[chatListId]) {
- state.chats[chatListId].messageIds.push(message.id);
- state.chats[chatListId].lastMessageTime = newMessage.createdAt;
- } else {
- // If the chat doesn't exist, create it
- state.chats[chatListId] = {
- id: chatListId,
- receiver: {
- id: message.senderId || "",
- isOnline: false,
- lastSeen: null,
- },
- messageIds: [message.id],
- lastMessageTime: newMessage.createdAt,
- };
- }
- },
-
- // Update receiver status
- updateReceiverStatus: (
- state,
- action: PayloadAction<{
- chatListId: string;
- isOnline: boolean;
- lastSeen?: string;
- }>
- ) => {
- const { chatListId, isOnline, lastSeen } = action.payload;
- if (state.chats[chatListId]) {
- state.chats[chatListId].receiver.isOnline = isOnline;
- if (lastSeen) {
- state.chats[chatListId].receiver.lastSeen = lastSeen;
- }
- }
- },
-
- // Remove a chat
- removeChat: (state, action: PayloadAction) => {
- const chatListId = action.payload;
-
- // Remove the chat
- delete state.chats[chatListId];
-
- // Remove all messages associated with the chat
- const messageIds = state.chats[chatListId]?.messageIds || [];
- messageIds.forEach((messageId) => {
- delete state.messages[messageId];
- });
- },
-
- // Set the selected chat
- setSelectedChat: (state, action: PayloadAction) => {
- state.selectedChatId = action.payload;
- },
-
- addOldMessages: (
- state,
- action: PayloadAction<{ chatListId: string; messages: Message[] }>
- ) => {
- const { chatListId, messages } = action.payload;
-
- // Add each message to the normalized messages
- messages.forEach((message) => {
- state.messages[message.id] = message;
- // Add the message ID to the chat's messageIds array
- if (state.chats[chatListId]) {
- state.chats[chatListId].messageIds.unshift(message.id);
- }
- });
- },
-
- markChatAsDelivered: (
- state,
- action: PayloadAction<{ chatListId: string }>
- ) => {
- const { chatListId } = action.payload;
- if (state.chats[chatListId]) {
- state.chats[chatListId].messageIds.forEach((messageId) => {
- if (state.messages[messageId]) {
- if (
- state.messages[messageId].status == "sent" &&
- state.messages[messageId].isSent
- ) {
- state.messages[messageId].status = "delivered";
- }
- }
- });
- }
- },
-
- markChatAsSeen: (state, action: PayloadAction<{ chatListId: string }>) => {
- const { chatListId } = action.payload;
- if (state.chats[chatListId]) {
- state.chats[chatListId].messageIds.forEach((messageId) => {
- if (state.messages[messageId]) {
- if (state.messages[messageId].isSent) {
- if (state.messages[messageId].status !== "seen")
- state.messages[messageId].status = "seen";
- }
- }
- });
- }
- },
-
- markMessageAsSeen: (
- state,
- action: PayloadAction<{ chatListId: string; messageId: string }>
- ) => {
- const { chatListId, messageId } = action.payload;
- if (state.chats[chatListId]) {
- if (state.messages[messageId]) {
- if (state.messages[messageId].isSent) {
- if (state.messages[messageId].status !== "seen")
- state.messages[messageId].status = "seen";
- }
- }
- }
- },
-
- updateUserDetails: (
- state,
- action: PayloadAction<{
- chatListId: string;
- username: string;
- profileImage: string | undefined;
- }>
- ) => {
- const { chatListId, username, profileImage } = action.payload;
- if (state.chats[chatListId]) {
- state.chats[chatListId].receiver.username = username;
- state.chats[chatListId].receiver.profileImage = profileImage;
- }
- },
- },
-});
-
-export const {
- addChat,
- addChats,
- addMessage,
- updateReceiverStatus,
- removeChat,
- setSelectedChat,
- addOldMessages,
- markChatAsDelivered,
- markChatAsSeen,
- updateUserDetails,
- markMessageAsSeen,
-} = chatSlice.actions;
-export default chatSlice.reducer;
diff --git a/Frontend/src/redux/store.ts b/Frontend/src/redux/store.ts
deleted file mode 100644
index 0e8e30d..0000000
--- a/Frontend/src/redux/store.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { configureStore } from "@reduxjs/toolkit";
-import chatReducer from "./chatSlice";
-
-const store = configureStore({
- reducer: {
- chat: chatReducer,
- },
-});
-
-// Infer the `RootState` and `AppDispatch` types from the store itself
-export type RootState = ReturnType;
-export type AppDispatch = typeof store.dispatch;
-export default store;
diff --git a/Frontend/src/types/chat.ts b/Frontend/src/types/chat.ts
deleted file mode 100644
index 399e50e..0000000
--- a/Frontend/src/types/chat.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-export interface MessagesReadResponse {
- chatListId: string;
- eventType: "MESSAGES_READ";
-}
-
-export interface NewMessageResponse {
- id: string;
- isSent: boolean;
- status?: "sent" | "delivered" | "seen";
- senderId?: string;
- chatListId?: string;
- message: string;
- createdAt: string;
- eventType?: string;
- username?: string;
-}
diff --git a/Frontend/src/utils/demoInsert.ts b/Frontend/src/utils/demoInsert.ts
deleted file mode 100644
index 3e309e9..0000000
--- a/Frontend/src/utils/demoInsert.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { supabase } from './supabase';
-
-export async function demoInsert() {
- // Insert user
- const { data: user, error: userError } = await supabase
- .from('users')
- .insert({
- id: 'demo-user-123',
- username: 'demouser',
- email: 'demo@example.com',
- role: 'creator',
- age: '25',
- gender: 'Male',
- country: 'India',
- category: 'Tech',
- });
- console.log('User:', user, userError);
-
- // Insert social profile
- const { data: profile, error: profileError } = await supabase
- .from('social_profiles')
- .insert({
- user_id: 'demo-user-123',
- platform: 'YouTube',
- username: 'demoyt',
- followers: 1000,
- posts: 10,
- channel_id: 'UC1234567890abcdef',
- channel_name: 'Demo Channel',
- subscriber_count: 1000,
- total_views: 50000,
- video_count: 10,
- per_post_cost: 100,
- per_video_cost: 200,
- per_post_cost_currency: 'USD',
- per_video_cost_currency: 'USD',
- channel_url: 'https://youtube.com/channel/UC1234567890abcdef',
- });
- console.log('Profile:', profile, profileError);
-}
\ No newline at end of file
diff --git a/Frontend/src/utils/supabase.tsx b/Frontend/src/utils/supabase.tsx
deleted file mode 100644
index b97998d..0000000
--- a/Frontend/src/utils/supabase.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { createClient } from "@supabase/supabase-js";
-
-const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
-const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
-
-if (!supabaseUrl || !supabaseAnonKey) {
- console.error("Supabase environment variables are not configured. Please set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in your .env file.");
- throw new Error("Missing Supabase environment variables");
-}
-
-export const supabase = createClient(supabaseUrl, supabaseAnonKey);
-export * from "@supabase/supabase-js";
diff --git a/Frontend/src/vite-env.d.ts b/Frontend/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe..0000000
--- a/Frontend/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/Frontend/tsconfig.app.json b/Frontend/tsconfig.app.json
deleted file mode 100644
index 0f468d7..0000000
--- a/Frontend/tsconfig.app.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": false,
- "noUnusedParameters": false,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true,
- "baseUrl": ".",
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": ["src"]
-}
diff --git a/Frontend/tsconfig.json b/Frontend/tsconfig.json
deleted file mode 100644
index fec8c8e..0000000
--- a/Frontend/tsconfig.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "files": [],
- "references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ],
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@/*": ["./src/*"]
- }
- }
-}
diff --git a/Frontend/tsconfig.node.json b/Frontend/tsconfig.node.json
deleted file mode 100644
index db0becc..0000000
--- a/Frontend/tsconfig.node.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2022",
- "lib": ["ES2023"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/Frontend/vite.config.ts b/Frontend/vite.config.ts
deleted file mode 100644
index 4eba012..0000000
--- a/Frontend/vite.config.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import path from "path";
-import tailwindcss from "@tailwindcss/vite";
-import react from "@vitejs/plugin-react";
-import { defineConfig } from "vite";
-
-// https://vite.dev/config/
-export default defineConfig({
- plugins: [react(), tailwindcss()],
- resolve: {
- alias: {
- "@": path.resolve(__dirname, "./src"),
- },
- },
- server: {
- proxy: {
- '/api': 'http://localhost:8000',
- },
- },
-});
diff --git a/LandingPage/.gitignore b/LandingPage/.gitignore
deleted file mode 100644
index a547bf3..0000000
--- a/LandingPage/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/LandingPage/README.md b/LandingPage/README.md
deleted file mode 100644
index 5443803..0000000
--- a/LandingPage/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Inpact Landing Page
-
-**This repository is a fork of [ishaanxgupta/Inpact-LandingPage](https://github.com/ishaanxgupta/Inpact-LandingPage).**
-
-Special thanks and credit to **Ishaan Gupta** ([ishaanxgupta](https://github.com/ishaanxgupta)) for building the landing page to this level.
-
----
\ No newline at end of file
diff --git a/LandingPage/eslint.config.js b/LandingPage/eslint.config.js
deleted file mode 100644
index 092408a..0000000
--- a/LandingPage/eslint.config.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
-
-export default tseslint.config(
- { ignores: ['dist'] },
- {
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
- files: ['**/*.{ts,tsx}'],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- },
- plugins: {
- 'react-hooks': reactHooks,
- 'react-refresh': reactRefresh,
- },
- rules: {
- ...reactHooks.configs.recommended.rules,
- 'react-refresh/only-export-components': [
- 'warn',
- { allowConstantExport: true },
- ],
- },
- },
-)
diff --git a/LandingPage/index.html b/LandingPage/index.html
deleted file mode 100644
index 340be0d..0000000
--- a/LandingPage/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
- Vite + React + TS
-
-
-
-
-
-
diff --git a/LandingPage/package-lock.json b/LandingPage/package-lock.json
deleted file mode 100644
index f3f9e25..0000000
--- a/LandingPage/package-lock.json
+++ /dev/null
@@ -1,4291 +0,0 @@
-{
- "name": "inpactai",
- "version": "0.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "inpactai",
- "version": "0.0.0",
- "dependencies": {
- "@emotion/react": "^11.14.0",
- "@emotion/styled": "^11.14.0",
- "@mui/material": "^7.0.2",
- "clsx": "^2.1.1",
- "framer-motion": "^12.6.5",
- "gsap": "^3.12.7",
- "lucide-react": "^0.487.0",
- "ogl": "^1.0.11",
- "react": "^19.0.0",
- "react-dom": "^19.0.0",
- "react-router-dom": "^7.5.0",
- "react-slick": "^0.30.3",
- "react-social-icons": "^6.24.0",
- "slick-carousel": "^1.8.1",
- "styled-components": "^6.1.17"
- },
- "devDependencies": {
- "@eslint/js": "^9.21.0",
- "@types/react": "^19.0.10",
- "@types/react-dom": "^19.0.4",
- "@vitejs/plugin-react": "^4.3.4",
- "eslint": "^9.21.0",
- "eslint-plugin-react-hooks": "^5.1.0",
- "eslint-plugin-react-refresh": "^0.4.19",
- "globals": "^15.15.0",
- "typescript": "~5.7.2",
- "typescript-eslint": "^8.24.1",
- "vite": "^6.2.0"
- }
- },
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
- "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
- "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.26.10",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
- "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.26.10",
- "@babel/helper-compilation-targets": "^7.26.5",
- "@babel/helper-module-transforms": "^7.26.0",
- "@babel/helpers": "^7.26.10",
- "@babel/parser": "^7.26.10",
- "@babel/template": "^7.26.9",
- "@babel/traverse": "^7.26.10",
- "@babel/types": "^7.26.10",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
- "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.27.0",
- "@babel/types": "^7.27.0",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
- "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.26.8",
- "@babel/helper-validator-option": "^7.25.9",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
- "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
- "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9",
- "@babel/traverse": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.26.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
- "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
- "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
- "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
- "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
- "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.27.0",
- "@babel/types": "^7.27.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
- "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.27.0"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz",
- "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz",
- "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
- }
- },
- "node_modules/@babel/runtime": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
- "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
- "license": "MIT",
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
- "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/parser": "^7.27.0",
- "@babel/types": "^7.27.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
- "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.27.0",
- "@babel/parser": "^7.27.0",
- "@babel/template": "^7.27.0",
- "@babel/types": "^7.27.0",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse/node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
- "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@emotion/babel-plugin": {
- "version": "11.13.5",
- "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
- "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.16.7",
- "@babel/runtime": "^7.18.3",
- "@emotion/hash": "^0.9.2",
- "@emotion/memoize": "^0.9.0",
- "@emotion/serialize": "^1.3.3",
- "babel-plugin-macros": "^3.1.0",
- "convert-source-map": "^1.5.0",
- "escape-string-regexp": "^4.0.0",
- "find-root": "^1.1.0",
- "source-map": "^0.5.7",
- "stylis": "4.2.0"
- }
- },
- "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
- "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
- "license": "MIT"
- },
- "node_modules/@emotion/cache": {
- "version": "11.14.0",
- "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
- "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
- "license": "MIT",
- "dependencies": {
- "@emotion/memoize": "^0.9.0",
- "@emotion/sheet": "^1.4.0",
- "@emotion/utils": "^1.4.2",
- "@emotion/weak-memoize": "^0.4.0",
- "stylis": "4.2.0"
- }
- },
- "node_modules/@emotion/hash": {
- "version": "0.9.2",
- "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
- "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
- "license": "MIT"
- },
- "node_modules/@emotion/is-prop-valid": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
- "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
- "license": "MIT",
- "dependencies": {
- "@emotion/memoize": "^0.9.0"
- }
- },
- "node_modules/@emotion/memoize": {
- "version": "0.9.0",
- "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
- "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
- "license": "MIT"
- },
- "node_modules/@emotion/react": {
- "version": "11.14.0",
- "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
- "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.3",
- "@emotion/babel-plugin": "^11.13.5",
- "@emotion/cache": "^11.14.0",
- "@emotion/serialize": "^1.3.3",
- "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
- "@emotion/utils": "^1.4.2",
- "@emotion/weak-memoize": "^0.4.0",
- "hoist-non-react-statics": "^3.3.1"
- },
- "peerDependencies": {
- "react": ">=16.8.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@emotion/serialize": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
- "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
- "license": "MIT",
- "dependencies": {
- "@emotion/hash": "^0.9.2",
- "@emotion/memoize": "^0.9.0",
- "@emotion/unitless": "^0.10.0",
- "@emotion/utils": "^1.4.2",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@emotion/sheet": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
- "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
- "license": "MIT"
- },
- "node_modules/@emotion/styled": {
- "version": "11.14.0",
- "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
- "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.18.3",
- "@emotion/babel-plugin": "^11.13.5",
- "@emotion/is-prop-valid": "^1.3.0",
- "@emotion/serialize": "^1.3.3",
- "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
- "@emotion/utils": "^1.4.2"
- },
- "peerDependencies": {
- "@emotion/react": "^11.0.0-rc.0",
- "react": ">=16.8.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@emotion/unitless": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
- "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
- "license": "MIT"
- },
- "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
- "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
- "license": "MIT",
- "peerDependencies": {
- "react": ">=16.8.0"
- }
- },
- "node_modules/@emotion/utils": {
- "version": "1.4.2",
- "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
- "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
- "license": "MIT"
- },
- "node_modules/@emotion/weak-memoize": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
- "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
- "license": "MIT"
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
- "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
- "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
- "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
- "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
- "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
- "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
- "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
- "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
- "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
- "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
- "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
- "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
- "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
- "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
- "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
- "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
- "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
- "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
- "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
- "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
- "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
- "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
- "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
- "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
- "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@eslint-community/eslint-utils": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz",
- "integrity": "sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eslint-visitor-keys": "^3.4.3"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- },
- "peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
- }
- },
- "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint-community/regexpp": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
- }
- },
- "node_modules/@eslint/config-array": {
- "version": "0.20.0",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
- "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/object-schema": "^2.1.6",
- "debug": "^4.3.1",
- "minimatch": "^3.1.2"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/config-helpers": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
- "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/core": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
- "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/eslintrc": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
- "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "^6.12.4",
- "debug": "^4.3.2",
- "espree": "^10.0.1",
- "globals": "^14.0.0",
- "ignore": "^5.2.0",
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
- "minimatch": "^3.1.2",
- "strip-json-comments": "^3.1.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint/eslintrc/node_modules/globals": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@eslint/js": {
- "version": "9.24.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz",
- "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/object-schema": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
- "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/plugin-kit": {
- "version": "0.2.8",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
- "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.13.0",
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
- "version": "0.13.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
- "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@humanfs/core": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
- "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node": {
- "version": "0.16.6",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
- "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@humanfs/core": "^0.19.1",
- "@humanwhocodes/retry": "^0.3.0"
- },
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
- "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/module-importer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
- "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.22"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/retry": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
- "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
- "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@mui/core-downloads-tracker": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.0.2.tgz",
- "integrity": "sha512-TfeFU9TgN1N06hyb/pV/63FfO34nijZRMqgHk0TJ3gkl4Fbd+wZ73+ZtOd7jag6hMmzO9HSrBc6Vdn591nhkAg==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- }
- },
- "node_modules/@mui/material": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.0.2.tgz",
- "integrity": "sha512-rjJlJ13+3LdLfobRplkXbjIFEIkn6LgpetgU/Cs3Xd8qINCCQK9qXQIjjQ6P0FXFTPFzEVMj0VgBR1mN+FhOcA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.27.0",
- "@mui/core-downloads-tracker": "^7.0.2",
- "@mui/system": "^7.0.2",
- "@mui/types": "^7.4.1",
- "@mui/utils": "^7.0.2",
- "@popperjs/core": "^2.11.8",
- "@types/react-transition-group": "^4.4.12",
- "clsx": "^2.1.1",
- "csstype": "^3.1.3",
- "prop-types": "^15.8.1",
- "react-is": "^19.1.0",
- "react-transition-group": "^4.4.5"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@emotion/react": "^11.5.0",
- "@emotion/styled": "^11.3.0",
- "@mui/material-pigment-css": "^7.0.2",
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/react": {
- "optional": true
- },
- "@emotion/styled": {
- "optional": true
- },
- "@mui/material-pigment-css": {
- "optional": true
- },
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/private-theming": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.0.2.tgz",
- "integrity": "sha512-6lt8heDC9wN8YaRqEdhqnm0cFCv08AMf4IlttFvOVn7ZdKd81PNpD/rEtPGLLwQAFyyKSxBG4/2XCgpbcdNKiA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.27.0",
- "@mui/utils": "^7.0.2",
- "prop-types": "^15.8.1"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/styled-engine": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.0.2.tgz",
- "integrity": "sha512-11Bt4YdHGlh7sB8P75S9mRCUxTlgv7HGbr0UKz6m6Z9KLeiw1Bm9y/t3iqLLVMvSHYB6zL8X8X+LmfTE++gyBw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.27.0",
- "@emotion/cache": "^11.13.5",
- "@emotion/serialize": "^1.3.3",
- "@emotion/sheet": "^1.4.0",
- "csstype": "^3.1.3",
- "prop-types": "^15.8.1"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@emotion/react": "^11.4.1",
- "@emotion/styled": "^11.3.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/react": {
- "optional": true
- },
- "@emotion/styled": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/system": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.0.2.tgz",
- "integrity": "sha512-yFUraAWYWuKIISPPEVPSQ1NLeqmTT4qiQ+ktmyS8LO/KwHxB+NNVOacEZaIofh5x1NxY8rzphvU5X2heRZ/RDA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.27.0",
- "@mui/private-theming": "^7.0.2",
- "@mui/styled-engine": "^7.0.2",
- "@mui/types": "^7.4.1",
- "@mui/utils": "^7.0.2",
- "clsx": "^2.1.1",
- "csstype": "^3.1.3",
- "prop-types": "^15.8.1"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@emotion/react": "^11.5.0",
- "@emotion/styled": "^11.3.0",
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/react": {
- "optional": true
- },
- "@emotion/styled": {
- "optional": true
- },
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/types": {
- "version": "7.4.1",
- "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.1.tgz",
- "integrity": "sha512-gUL8IIAI52CRXP/MixT1tJKt3SI6tVv4U/9soFsTtAsHzaJQptZ42ffdHZV3niX1ei0aUgMvOxBBN0KYqdG39g==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.27.0"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/utils": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.2.tgz",
- "integrity": "sha512-72gcuQjPzhj/MLmPHLCgZjy2VjOH4KniR/4qRtXTTXIEwbkgcN+Y5W/rC90rWtMmZbjt9svZev/z+QHUI4j74w==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.27.0",
- "@mui/types": "^7.4.1",
- "@types/prop-types": "^15.7.14",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "react-is": "^19.1.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@popperjs/core": {
- "version": "2.11.8",
- "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
- "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/popperjs"
- }
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz",
- "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz",
- "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz",
- "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz",
- "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz",
- "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz",
- "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz",
- "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz",
- "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz",
- "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz",
- "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz",
- "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz",
- "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz",
- "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz",
- "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz",
- "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz",
- "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz",
- "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz",
- "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz",
- "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz",
- "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@types/babel__core": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7",
- "@types/babel__generator": "*",
- "@types/babel__template": "*",
- "@types/babel__traverse": "*"
- }
- },
- "node_modules/@types/babel__generator": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.1.0",
- "@babel/types": "^7.0.0"
- }
- },
- "node_modules/@types/babel__traverse": {
- "version": "7.20.7",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
- "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.20.7"
- }
- },
- "node_modules/@types/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
- "license": "MIT"
- },
- "node_modules/@types/estree": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
- "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/parse-json": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
- "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
- "license": "MIT"
- },
- "node_modules/@types/prop-types": {
- "version": "15.7.14",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
- "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
- "license": "MIT"
- },
- "node_modules/@types/react": {
- "version": "19.1.1",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz",
- "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==",
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "19.1.2",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz",
- "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "^19.0.0"
- }
- },
- "node_modules/@types/react-transition-group": {
- "version": "4.4.12",
- "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
- "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*"
- }
- },
- "node_modules/@types/stylis": {
- "version": "4.2.5",
- "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
- "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==",
- "license": "MIT"
- },
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.29.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz",
- "integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.29.1",
- "@typescript-eslint/type-utils": "8.29.1",
- "@typescript-eslint/utils": "8.29.1",
- "@typescript-eslint/visitor-keys": "8.29.1",
- "graphemer": "^1.4.0",
- "ignore": "^5.3.1",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^2.0.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/parser": {
- "version": "8.29.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz",
- "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/scope-manager": "8.29.1",
- "@typescript-eslint/types": "8.29.1",
- "@typescript-eslint/typescript-estree": "8.29.1",
- "@typescript-eslint/visitor-keys": "8.29.1",
- "debug": "^4.3.4"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.29.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz",
- "integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.29.1",
- "@typescript-eslint/visitor-keys": "8.29.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/type-utils": {
- "version": "8.29.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz",
- "integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/typescript-estree": "8.29.1",
- "@typescript-eslint/utils": "8.29.1",
- "debug": "^4.3.4",
- "ts-api-utils": "^2.0.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/types": {
- "version": "8.29.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz",
- "integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.29.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz",
- "integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.29.1",
- "@typescript-eslint/visitor-keys": "8.29.1",
- "debug": "^4.3.4",
- "fast-glob": "^3.3.2",
- "is-glob": "^4.0.3",
- "minimatch": "^9.0.4",
- "semver": "^7.6.0",
- "ts-api-utils": "^2.0.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@typescript-eslint/utils": {
- "version": "8.29.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz",
- "integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "8.29.1",
- "@typescript-eslint/types": "8.29.1",
- "@typescript-eslint/typescript-estree": "8.29.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.29.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz",
- "integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.29.1",
- "eslint-visitor-keys": "^4.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@vitejs/plugin-react": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
- "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.26.0",
- "@babel/plugin-transform-react-jsx-self": "^7.25.9",
- "@babel/plugin-transform-react-jsx-source": "^7.25.9",
- "@types/babel__core": "^7.20.5",
- "react-refresh": "^0.14.2"
- },
- "engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
- }
- },
- "node_modules/acorn": {
- "version": "8.14.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
- "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
- "license": "Python-2.0"
- },
- "node_modules/babel-plugin-macros": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
- "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.12.5",
- "cosmiconfig": "^7.0.0",
- "resolve": "^1.19.0"
- },
- "engines": {
- "node": ">=10",
- "npm": ">=6"
- }
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/browserslist": {
- "version": "4.24.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
- "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "caniuse-lite": "^1.0.30001688",
- "electron-to-chromium": "^1.5.73",
- "node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.1"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/camelize": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
- "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001713",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
- "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
- },
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/classnames": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
- "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
- "license": "MIT"
- },
- "node_modules/clsx": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cookie": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
- "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/cosmiconfig": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
- "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
- "license": "MIT",
- "dependencies": {
- "@types/parse-json": "^4.0.0",
- "import-fresh": "^3.2.1",
- "parse-json": "^5.0.0",
- "path-type": "^4.0.0",
- "yaml": "^1.10.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/cosmiconfig/node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
- "license": "ISC",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/css-color-keywords": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
- "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
- "license": "ISC",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/css-to-react-native": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
- "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
- "license": "MIT",
- "dependencies": {
- "camelize": "^1.0.0",
- "css-color-keywords": "^1.0.0",
- "postcss-value-parser": "^4.0.2"
- }
- },
- "node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "license": "MIT"
- },
- "node_modules/debug": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
- "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/dom-helpers": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
- "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.8.7",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.136",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.136.tgz",
- "integrity": "sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/enquire.js": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz",
- "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==",
- "license": "MIT"
- },
- "node_modules/error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "license": "MIT",
- "dependencies": {
- "is-arrayish": "^0.2.1"
- }
- },
- "node_modules/esbuild": {
- "version": "0.25.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
- "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.2",
- "@esbuild/android-arm": "0.25.2",
- "@esbuild/android-arm64": "0.25.2",
- "@esbuild/android-x64": "0.25.2",
- "@esbuild/darwin-arm64": "0.25.2",
- "@esbuild/darwin-x64": "0.25.2",
- "@esbuild/freebsd-arm64": "0.25.2",
- "@esbuild/freebsd-x64": "0.25.2",
- "@esbuild/linux-arm": "0.25.2",
- "@esbuild/linux-arm64": "0.25.2",
- "@esbuild/linux-ia32": "0.25.2",
- "@esbuild/linux-loong64": "0.25.2",
- "@esbuild/linux-mips64el": "0.25.2",
- "@esbuild/linux-ppc64": "0.25.2",
- "@esbuild/linux-riscv64": "0.25.2",
- "@esbuild/linux-s390x": "0.25.2",
- "@esbuild/linux-x64": "0.25.2",
- "@esbuild/netbsd-arm64": "0.25.2",
- "@esbuild/netbsd-x64": "0.25.2",
- "@esbuild/openbsd-arm64": "0.25.2",
- "@esbuild/openbsd-x64": "0.25.2",
- "@esbuild/sunos-x64": "0.25.2",
- "@esbuild/win32-arm64": "0.25.2",
- "@esbuild/win32-ia32": "0.25.2",
- "@esbuild/win32-x64": "0.25.2"
- }
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint": {
- "version": "9.24.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz",
- "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.20.0",
- "@eslint/config-helpers": "^0.2.0",
- "@eslint/core": "^0.12.0",
- "@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.24.0",
- "@eslint/plugin-kit": "^0.2.7",
- "@humanfs/node": "^0.16.6",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.4.2",
- "@types/estree": "^1.0.6",
- "@types/json-schema": "^7.0.15",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.3.0",
- "eslint-visitor-keys": "^4.2.0",
- "espree": "^10.3.0",
- "esquery": "^1.5.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "jiti": "*"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-plugin-react-hooks": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
- "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
- }
- },
- "node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.19",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz",
- "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "eslint": ">=8.40"
- }
- },
- "node_modules/eslint-scope": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
- "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-visitor-keys": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
- "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/espree": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
- "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "acorn": "^8.14.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/esquery": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
- "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "estraverse": "^5.1.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-glob": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
- "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.8"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fastq": {
- "version": "1.19.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
- "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "reusify": "^1.0.4"
- }
- },
- "node_modules/file-entry-cache": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flat-cache": "^4.0.0"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/find-root": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
- "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
- "license": "MIT"
- },
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
- },
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/framer-motion": {
- "version": "12.6.5",
- "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.5.tgz",
- "integrity": "sha512-MKvnWov0paNjvRJuIy6x418w23tFqRfS6CXHhZrCiSEpXVlo/F+usr8v4/3G6O0u7CpsaO1qop+v4Ip7PRCBqQ==",
- "license": "MIT",
- "dependencies": {
- "motion-dom": "^12.6.5",
- "motion-utils": "^12.6.5",
- "tslib": "^2.4.0"
- },
- "peerDependencies": {
- "@emotion/is-prop-valid": "*",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/is-prop-valid": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/globals": {
- "version": "15.15.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
- "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/gsap": {
- "version": "3.12.7",
- "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.7.tgz",
- "integrity": "sha512-V4GsyVamhmKefvcAKaoy0h6si0xX7ogwBoBSs2CTJwt7luW0oZzC0LhdkyuKV8PJAXr7Yaj8pMjCKD4GJ+eEMg==",
- "license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/"
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "react-is": "^16.7.0"
- }
- },
- "node_modules/hoist-non-react-statics/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
- "node_modules/ignore": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/import-fresh": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
- "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
- "license": "MIT",
- "dependencies": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/is-arrayish": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
- "license": "MIT"
- },
- "node_modules/is-core-module": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
- "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
- "license": "MIT",
- "dependencies": {
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/jquery": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
- "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
- },
- "node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-parse-even-better-errors": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
- "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "license": "MIT"
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json2mq": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
- "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
- "license": "MIT",
- "dependencies": {
- "string-convert": "^0.2.0"
- }
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "json-buffer": "3.0.1"
- }
- },
- "node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "license": "MIT"
- },
- "node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lodash.debounce": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
- "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
- "license": "MIT"
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/lucide-react": {
- "version": "0.487.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.487.0.tgz",
- "integrity": "sha512-aKqhOQ+YmFnwq8dWgGjOuLc8V1R9/c/yOd+zDY4+ohsR2Jo05lSGc3WsstYPIzcTpeosN7LoCkLReUUITvaIvw==",
- "license": "ISC",
- "peerDependencies": {
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/micromatch": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/motion-dom": {
- "version": "12.6.5",
- "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.5.tgz",
- "integrity": "sha512-jpM9TQLXzYMWMJ7Ec7sAj0iis8oIuu6WvjI3yNKJLdrZyrsI/b2cRInDVL8dCl683zQQq19DpL9cSMP+k8T1NA==",
- "license": "MIT",
- "dependencies": {
- "motion-utils": "^12.6.5"
- }
- },
- "node_modules/motion-utils": {
- "version": "12.6.5",
- "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.6.5.tgz",
- "integrity": "sha512-IsOeKsOF+FWBhxQEDFBO6ZYC8/jlidmVbbLpe9/lXSA9j9kzGIMUuIBx2SZY+0reAS0DjZZ1i7dJp4NHrjocPw==",
- "license": "MIT"
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/ogl": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz",
- "integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==",
- "license": "Unlicense"
- },
- "node_modules/optionator": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
- "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.5"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "license": "MIT",
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/parse-json": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
- "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.0.0",
- "error-ex": "^1.3.1",
- "json-parse-even-better-errors": "^2.3.0",
- "lines-and-columns": "^1.1.6"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "license": "MIT"
- },
- "node_modules/path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.3",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
- "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.8",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/postcss-value-parser": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "license": "MIT"
- },
- "node_modules/prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
- "node_modules/prop-types/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/react": {
- "version": "19.1.0",
- "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
- "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "19.1.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
- "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
- "license": "MIT",
- "dependencies": {
- "scheduler": "^0.26.0"
- },
- "peerDependencies": {
- "react": "^19.1.0"
- }
- },
- "node_modules/react-is": {
- "version": "19.1.0",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
- "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
- "license": "MIT"
- },
- "node_modules/react-refresh": {
- "version": "0.14.2",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
- "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-router": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz",
- "integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==",
- "license": "MIT",
- "dependencies": {
- "@types/cookie": "^0.6.0",
- "cookie": "^1.0.1",
- "set-cookie-parser": "^2.6.0",
- "turbo-stream": "2.4.0"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/react-router-dom": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.0.tgz",
- "integrity": "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==",
- "license": "MIT",
- "dependencies": {
- "react-router": "7.5.0"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- }
- },
- "node_modules/react-slick": {
- "version": "0.30.3",
- "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.3.tgz",
- "integrity": "sha512-B4x0L9GhkEWUMApeHxr/Ezp2NncpGc+5174R02j+zFiWuYboaq98vmxwlpafZfMjZic1bjdIqqmwLDcQY0QaFA==",
- "license": "MIT",
- "dependencies": {
- "classnames": "^2.2.5",
- "enquire.js": "^2.1.6",
- "json2mq": "^0.2.0",
- "lodash.debounce": "^4.0.8",
- "resize-observer-polyfill": "^1.5.0"
- },
- "peerDependencies": {
- "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/react-social-icons": {
- "version": "6.24.0",
- "resolved": "https://registry.npmjs.org/react-social-icons/-/react-social-icons-6.24.0.tgz",
- "integrity": "sha512-1YlJe2TOf/UwPi2JAb8Ci7J207owP806Tpxu36o4EkB1/jLjGhi83xbCHOagoMyPozTZrPnZIGgvp1LiiWGuZw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.24.8"
- },
- "peerDependencies": {
- "react": "16.x.x || 17.x.x || 18.x.x || 19.x.x",
- "react-dom": "16.x.x || 17.x.x || 18.x.x || 19.x.x"
- }
- },
- "node_modules/react-transition-group": {
- "version": "4.4.5",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
- "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@babel/runtime": "^7.5.5",
- "dom-helpers": "^5.0.1",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.6.2"
- },
- "peerDependencies": {
- "react": ">=16.6.0",
- "react-dom": ">=16.6.0"
- }
- },
- "node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "license": "MIT"
- },
- "node_modules/resize-observer-polyfill": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
- "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
- "license": "MIT"
- },
- "node_modules/resolve": {
- "version": "1.22.10",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
- "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
- "license": "MIT",
- "dependencies": {
- "is-core-module": "^2.16.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/reusify": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
- "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
- "node_modules/rollup": {
- "version": "4.40.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
- "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.7"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.40.0",
- "@rollup/rollup-android-arm64": "4.40.0",
- "@rollup/rollup-darwin-arm64": "4.40.0",
- "@rollup/rollup-darwin-x64": "4.40.0",
- "@rollup/rollup-freebsd-arm64": "4.40.0",
- "@rollup/rollup-freebsd-x64": "4.40.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.40.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.40.0",
- "@rollup/rollup-linux-arm64-gnu": "4.40.0",
- "@rollup/rollup-linux-arm64-musl": "4.40.0",
- "@rollup/rollup-linux-loongarch64-gnu": "4.40.0",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.40.0",
- "@rollup/rollup-linux-riscv64-musl": "4.40.0",
- "@rollup/rollup-linux-s390x-gnu": "4.40.0",
- "@rollup/rollup-linux-x64-gnu": "4.40.0",
- "@rollup/rollup-linux-x64-musl": "4.40.0",
- "@rollup/rollup-win32-arm64-msvc": "4.40.0",
- "@rollup/rollup-win32-ia32-msvc": "4.40.0",
- "@rollup/rollup-win32-x64-msvc": "4.40.0",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "queue-microtask": "^1.2.2"
- }
- },
- "node_modules/scheduler": {
- "version": "0.26.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
- "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
- "license": "MIT"
- },
- "node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/set-cookie-parser": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
- "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
- "license": "MIT"
- },
- "node_modules/shallowequal": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
- "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
- "license": "MIT"
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/slick-carousel": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
- "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==",
- "license": "MIT",
- "peerDependencies": {
- "jquery": ">=1.8.0"
- }
- },
- "node_modules/source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/string-convert": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
- "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
- "license": "MIT"
- },
- "node_modules/strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/styled-components": {
- "version": "6.1.17",
- "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.17.tgz",
- "integrity": "sha512-97D7DwWanI7nN24v0D4SvbfjLE9656umNSJZkBkDIWL37aZqG/wRQ+Y9pWtXyBIM/NSfcBzHLErEsqHmJNSVUg==",
- "license": "MIT",
- "dependencies": {
- "@emotion/is-prop-valid": "1.2.2",
- "@emotion/unitless": "0.8.1",
- "@types/stylis": "4.2.5",
- "css-to-react-native": "3.2.0",
- "csstype": "3.1.3",
- "postcss": "8.4.49",
- "shallowequal": "1.1.0",
- "stylis": "4.3.2",
- "tslib": "2.6.2"
- },
- "engines": {
- "node": ">= 16"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/styled-components"
- },
- "peerDependencies": {
- "react": ">= 16.8.0",
- "react-dom": ">= 16.8.0"
- }
- },
- "node_modules/styled-components/node_modules/@emotion/is-prop-valid": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
- "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
- "license": "MIT",
- "dependencies": {
- "@emotion/memoize": "^0.8.1"
- }
- },
- "node_modules/styled-components/node_modules/@emotion/memoize": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
- "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
- "license": "MIT"
- },
- "node_modules/styled-components/node_modules/@emotion/unitless": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
- "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
- "license": "MIT"
- },
- "node_modules/styled-components/node_modules/postcss": {
- "version": "8.4.49",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
- "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/styled-components/node_modules/stylis": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
- "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==",
- "license": "MIT"
- },
- "node_modules/styled-components/node_modules/tslib": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
- "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
- "license": "0BSD"
- },
- "node_modules/stylis": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
- "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
- "license": "MIT"
- },
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/ts-api-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
- "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18.12"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
- "node_modules/turbo-stream": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
- "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
- "license": "ISC"
- },
- "node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/typescript": {
- "version": "5.7.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
- "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/typescript-eslint": {
- "version": "8.29.1",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.1.tgz",
- "integrity": "sha512-f8cDkvndhbQMPcysk6CUSGBWV+g1utqdn71P5YKwMumVMOG/5k7cHq0KyG4O52nB0oKS4aN2Tp5+wB4APJGC+w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/eslint-plugin": "8.29.1",
- "@typescript-eslint/parser": "8.29.1",
- "@typescript-eslint/utils": "8.29.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
- }
- },
- "node_modules/update-browserslist-db": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
- "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
- "node_modules/vite": {
- "version": "6.2.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
- "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "^0.25.0",
- "postcss": "^8.5.3",
- "rollup": "^4.30.1"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "jiti": ">=1.21.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/word-wrap": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
- "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/yaml": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
- "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
- "dev": true,
- "license": "ISC",
- "optional": true,
- "peer": true,
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/yocto-queue": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- }
- }
-}
diff --git a/LandingPage/package.json b/LandingPage/package.json
deleted file mode 100644
index 558bc8e..0000000
--- a/LandingPage/package.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "name": "inpactai",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc -b && vite build",
- "lint": "eslint .",
- "preview": "vite preview"
- },
- "dependencies": {
- "@emotion/react": "^11.14.0",
- "@emotion/styled": "^11.14.0",
- "@mui/material": "^7.0.2",
- "clsx": "^2.1.1",
- "framer-motion": "^12.6.5",
- "gsap": "^3.12.7",
- "lucide-react": "^0.487.0",
- "ogl": "^1.0.11",
- "react": "^19.0.0",
- "react-dom": "^19.0.0",
- "react-router-dom": "^7.5.0",
- "react-slick": "^0.30.3",
- "react-social-icons": "^6.24.0",
- "slick-carousel": "^1.8.1",
- "styled-components": "^6.1.17"
- },
- "devDependencies": {
- "@eslint/js": "^9.21.0",
- "@types/react": "^19.0.10",
- "@types/react-dom": "^19.0.4",
- "@vitejs/plugin-react": "^4.3.4",
- "eslint": "^9.21.0",
- "eslint-plugin-react-hooks": "^5.1.0",
- "eslint-plugin-react-refresh": "^0.4.19",
- "globals": "^15.15.0",
- "typescript": "~5.7.2",
- "typescript-eslint": "^8.24.1",
- "vite": "^6.2.0"
- }
-}
diff --git a/LandingPage/public/aossie_logo.png b/LandingPage/public/aossie_logo.png
deleted file mode 100644
index b2421da..0000000
Binary files a/LandingPage/public/aossie_logo.png and /dev/null differ
diff --git a/LandingPage/public/vite.svg b/LandingPage/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/LandingPage/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/LandingPage/src/App.css b/LandingPage/src/App.css
deleted file mode 100644
index 59983a1..0000000
--- a/LandingPage/src/App.css
+++ /dev/null
@@ -1,60 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
-/* Applies to WebKit browsers */
-::-webkit-scrollbar {
- width: 8px;
-}
-
-::-webkit-scrollbar-track {
- background: #000000;
- border-radius: 3px;
-}
-
-::-webkit-scrollbar-thumb {
- background: linear-gradient(to bottom, #fc5fff, #764e95); /* Gradient colors */
- border-radius: 3px;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: linear-gradient(to bottom, #de43e9, #764e95); /* Darker on hover */
-}
diff --git a/LandingPage/src/App.tsx b/LandingPage/src/App.tsx
deleted file mode 100644
index 3ac0eab..0000000
--- a/LandingPage/src/App.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { BrowserRouter, Routes, Route } from 'react-router-dom';
-// import Lenis from '@studio-freight/lenis';
-import Landing from '../src/Pages/Landing';
-import PrivacyPolicy from './Pages/Privacy';
-import TermsOfService from './Pages/Legal';
-
-function App() {
- // useEffect(() => {
- // const lenis = new Lenis({
- // duration: 1.2,
- // easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
- // smoothWheel: true,
- // });
-
- // function raf(time: number) {
- // lenis.raf(time);
- // requestAnimationFrame(raf);
- // }
-
- // requestAnimationFrame(raf);
- // }, []);
-
- return (
-
-
- } />
- } />
- } />
- {/* } /> */}
-
- {/* */}
-
- );
-}
-
-export default App;
\ No newline at end of file
diff --git a/LandingPage/src/Pages/Landing.tsx b/LandingPage/src/Pages/Landing.tsx
deleted file mode 100644
index 90f25ac..0000000
--- a/LandingPage/src/Pages/Landing.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { motion } from 'framer-motion';
-import { Sparkles, Users } from 'lucide-react';
-import { DollarSign, FileText, BarChart2, RefreshCw } from 'lucide-react';
-import ChatbotSidebarForm from '../components/form';
-import Footer from '../components/Footer';
-import Threads from '../components/bg';
-import SpotlightCard from '../components/card';
-import Header from '../components/Header';
-import HowItWorks from '../components/howitworks';
-import Integrations from '../components/integration';
-
-
-function Landing() {
- const headingWords = ['Creator', 'Collaboration', 'Hub'];
-
- const containerVariants = {
- hidden: { opacity: 0 },
- show: {
- opacity: 1,
- transition: {
- staggerChildren: 0.2,
- },
- },
- };
-
- const wordVariants = {
- hidden: { opacity: 0, y: 30 },
- show: { opacity: 1, y: 0 },
- };
- return (
-
-
- {/* Hero Section */}
-
-
-
- Powered by AOSSIE
-
-
-
-
-
-
-
-
- {headingWords.map((word, index) => (
-
- {word}
-
- ))}
-
-
-
-
- The future of creator collaboration is coming.
- Join the waitlist to be the first to experience
- the revolution in influencer marketing .
-
-
-
- {/* Features */}
-
-
- Features
-
-
-
-
-
- {[
- {
- icon:
,
- title: "AI-Driven Sponsorship Matchmaking",
- description:
- "Automatically connects creators with brands based on audience demographics, engagement rates, and content style.",
- },
- {
- icon:
,
- title: "AI-Powered Creator Collaboration Hub",
- description:
- "Facilitates partnerships between creators with complementary audiences and content niches.",
- },
- {
- icon:
,
- title: "AI-Based Pricing & Deal Optimization",
- description:
- "Provides fair sponsorship pricing recommendations based on engagement, market trends, and historical data.",
- },
- {
- icon:
,
- title: "AI-Powered Negotiation & Contract Assistant",
- description:
- "Assists in structuring deals, generating contracts, and optimizing terms using AI insights.",
- },
- {
- icon:
,
- title: "Performance Analytics & ROI Tracking",
- description:
- "Enables brands and creators to track sponsorship performance, audience engagement, and campaign success.",
- },
- {
- icon:
,
- title: "Real-Time Campaign Feedback Loop",
- description:
- "Gathers continuous feedback on campaigns to adapt and improve collaboration effectiveness over time.",
- },
- ].map((feature, index) => (
-
- {feature.icon}
- {feature.title}
- {feature.description}
-
- ))}
-
-
- {/* How It Works Section */}
-
-
-
-
-
- {/* Waitlist Form Section */}
-
-
-
- Be a Part of the Revolution
-
-
-
- Join our waitlist to be the first to experience how InpactAI is transforming the way brands and creators collaborate.
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default Landing;
diff --git a/LandingPage/src/Pages/Legal.tsx b/LandingPage/src/Pages/Legal.tsx
deleted file mode 100644
index f533279..0000000
--- a/LandingPage/src/Pages/Legal.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React, { useEffect } from 'react';
-import Navbar from '../components/Header';
-import Footer from '../components/Footer';
-import { motion } from 'framer-motion';
-
-const TermsOfService: React.FC = () => {
- useEffect(() => {
- window.scrollTo(0, 0);
- document.title = "Terms of Service | inpactAI";
- }, []);
-
- return (
-
-
-
-
-
- Terms of Service
-
-
-
Last updated: April 14, 2025
-
-
- Introduction
-
- Welcome to inpactAI. These Terms of Service ("Terms") govern your use of our platform and services. By accessing or using inpactAI, you agree to be bound by these Terms.
-
-
-
-
- Use of the Platform
- You agree to use the platform in compliance with all applicable laws and regulations. You must not misuse our platform or attempt to access it using a method other than the interface we provide.
-
-
-
- Account Registration
- To access certain features, you may be required to create an account. You are responsible for safeguarding your account credentials and for any activities or actions under your account.
-
-
-
- User Content
- You retain ownership of the content you submit to the platform, but you grant inpactAI a worldwide, royalty-free license to use, display, and distribute that content as needed to provide services.
-
-
-
- Prohibited Activities
- You agree not to engage in any of the following:
-
- Reverse engineering or decompiling any part of the platform
- Using the platform for unlawful or harmful purposes
- Infringing on the intellectual property rights of others
- Disrupting the functionality or security of the platform
-
-
-
-
- Termination
-
- We may suspend or terminate your access if you violate these Terms. Upon termination, your right to use the platform ceases immediately.
-
-
-
-
- Disclaimers
-
- The platform is provided "as is" without warranties of any kind. We do not guarantee that the platform will be uninterrupted, secure, or error-free.
-
-
-
-
- Limitation of Liability
-
- To the fullest extent permitted by law, inpactAI shall not be liable for any indirect, incidental, special, or consequential damages resulting from your use of the platform.
-
-
-
-
- Changes to These Terms
-
- We may modify these Terms at any time. If we do, we will notify you via the platform or by email. Continued use of the platform after changes means you accept the new Terms.
-
-
-
-
- Contact Us
-
- If you have any questions about these Terms, contact us at:
-
- Email: aossie.oss@gmail.com
-
-
-
-
-
-
-
-
- );
-};
-
-export default TermsOfService;
\ No newline at end of file
diff --git a/LandingPage/src/Pages/Privacy.tsx b/LandingPage/src/Pages/Privacy.tsx
deleted file mode 100644
index 4705e21..0000000
--- a/LandingPage/src/Pages/Privacy.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React, { useEffect } from 'react';
-import Navbar from '../components/Header';
-import Footer from '../components/Footer';
-import { motion } from 'framer-motion';
-
-const PrivacyPolicy: React.FC = () => {
- useEffect(() => {
- window.scrollTo(0, 0);
- document.title = "Privacy Policy | inpactAI";
- }, []);
-
- return (
-
-
-
-
-
- Privacy Policy
-
-
-
Last updated: April 14, 2025
-
-
- Introduction
-
- At inpactAI, we respect your privacy and are committed to protecting your personal data. This Privacy
- Policy explains how we collect, use, disclose, and safeguard your information when you use our service.
-
-
-
-
- Information We Collect
- When you use inpactAI, we may collect the following types of information:
-
-
- Personal Information: Name, email address, and organization details provided
- during signup or when joining our waitlist.
-
-
- Platform Data: If you connect inpactAI with platforms like Instagram, TikTok, or LinkedIn,
- we may collect information necessary to offer creator-brand matching services, such as follower data,
- audience engagement, and content performance metrics.
-
-
- Usage Information: Data about how you interact with our platform, including features used,
- actions taken, and time spent on the platform.
-
-
-
-
-
- How We Use Your Information
- We use the collected information for various purposes, including:
-
- Providing and maintaining our AI-powered platform
- Improving creator-brand matching accuracy
- Personalizing your dashboard and analytics
- Communicating with you about updates, matches, and insights
- Ensuring data integrity and platform security
-
-
-
-
- Data Sharing and Disclosure
-
- We do not sell your personal information. We may share data in the following circumstances:
-
-
- With service providers helping us run the platform (e.g., analytics, hosting)
- To comply with legal obligations or respond to lawful requests
- With your consent or to fulfill specific actions at your direction
- In connection with business restructuring, mergers, or acquisitions
-
-
-
-
- Data Security
-
- We apply industry-standard security practices to protect your data, including encryption, access controls,
- and regular security reviews. However, no digital system is completely secure, and we encourage
- you to practice responsible data handling on your end as well.
-
-
-
-
- Your Rights
-
- Depending on your location, you may have certain rights related to your personal information:
-
-
- Access or request a copy of your personal data
- Request correction of any inaccurate or incomplete data
- Request deletion of your personal data
- Restrict processing or object to data usage
- Request data portability
-
-
- To exercise your rights, contact us at privacy@inpact.ai.
-
-
-
-
- Changes to This Policy
-
- We may update this Privacy Policy occasionally to reflect changes in law or our practices.
- If significant changes are made, we will notify you on the platform or via email.
- Please review this policy periodically to stay informed.
-
-
-
-
- Contact Us
-
- If you have any questions about this Privacy Policy, feel free to reach out:
-
- Email: aossie.oss@gmail.com
-
-
-
-
-
-
-
-
- );
-};
-
-export default PrivacyPolicy;
diff --git a/LandingPage/src/assets/react.svg b/LandingPage/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/LandingPage/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/LandingPage/src/components/Footer.tsx b/LandingPage/src/components/Footer.tsx
deleted file mode 100644
index bbe3cf6..0000000
--- a/LandingPage/src/components/Footer.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Box, Typography, Link, Divider, Grid, Stack } from '@mui/material';
-
-const Footer = () => {
- const linkStyle = {
- color: 'rgba(255, 255, 255, 0.8)',
- textDecoration: 'none',
- fontSize: '0.95rem',
- '&:hover': {
- color: '#8B5CF6',
- },
- };
-
- return (
-
- {/* Top Section */}
-
-
-
- InpactAI
-
-
- Empowering brands to make smarter creator decisions through AI-powered insights and integrations.
-
-
-
-
-
- Explore
-
-
- Home
- About
- Contact
-
-
-
-
-
- Legal & Code
-
-
-
- GitHub
-
- Terms of Use
- Privacy Policy
-
-
-
-
- {/* Divider */}
-
-
- {/* Bottom Section */}
-
- © {new Date().getFullYear()} InpactAI. All rights reserved.
-
-
- );
-};
-
-export default Footer;
diff --git a/LandingPage/src/components/Header.tsx b/LandingPage/src/components/Header.tsx
deleted file mode 100644
index 4616b47..0000000
--- a/LandingPage/src/components/Header.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { motion } from 'framer-motion';
-import Github from './github';
-
-const Header = () => {
- return (
-
-
-
-
- InpactAI
-
-
-
-
-
-
-
- );
-};
-
-export default Header;
diff --git a/LandingPage/src/components/bg.tsx b/LandingPage/src/components/bg.tsx
deleted file mode 100644
index d69b642..0000000
--- a/LandingPage/src/components/bg.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-import React, { useEffect, useRef } from "react";
-import { Renderer, Program, Mesh, Triangle, Color } from "ogl";
-
-interface ThreadsProps {
- color?: [number, number, number];
- amplitude?: number;
- distance?: number;
- enableMouseInteraction?: boolean;
-}
-
-const vertexShader = `
-attribute vec2 position;
-attribute vec2 uv;
-varying vec2 vUv;
-void main() {
- vUv = uv;
- gl_Position = vec4(position, 0.0, 1.0);
-}
-`;
-
-const fragmentShader = `
-precision highp float;
-
-uniform float iTime;
-uniform vec3 iResolution;
-uniform vec3 uColor;
-uniform float uAmplitude;
-uniform float uDistance;
-uniform vec2 uMouse;
-
-#define PI 3.1415926538
-
-const int u_line_count = 40;
-const float u_line_width = 7.0;
-const float u_line_blur = 10.0;
-
-float Perlin2D(vec2 P) {
- vec2 Pi = floor(P);
- vec4 Pf_Pfmin1 = P.xyxy - vec4(Pi, Pi + 1.0);
- vec4 Pt = vec4(Pi.xy, Pi.xy + 1.0);
- Pt = Pt - floor(Pt * (1.0 / 71.0)) * 71.0;
- Pt += vec2(26.0, 161.0).xyxy;
- Pt *= Pt;
- Pt = Pt.xzxz * Pt.yyww;
- vec4 hash_x = fract(Pt * (1.0 / 951.135664));
- vec4 hash_y = fract(Pt * (1.0 / 642.949883));
- vec4 grad_x = hash_x - 0.49999;
- vec4 grad_y = hash_y - 0.49999;
- vec4 grad_results = inversesqrt(grad_x * grad_x + grad_y * grad_y)
- * (grad_x * Pf_Pfmin1.xzxz + grad_y * Pf_Pfmin1.yyww);
- grad_results *= 1.4142135623730950;
- vec2 blend = Pf_Pfmin1.xy * Pf_Pfmin1.xy * Pf_Pfmin1.xy
- * (Pf_Pfmin1.xy * (Pf_Pfmin1.xy * 6.0 - 15.0) + 10.0);
- vec4 blend2 = vec4(blend, vec2(1.0 - blend));
- return dot(grad_results, blend2.zxzx * blend2.wwyy);
-}
-
-float pixel(float count, vec2 resolution) {
- return (1.0 / max(resolution.x, resolution.y)) * count;
-}
-
-float lineFn(vec2 st, float width, float perc, float offset, vec2 mouse, float time, float amplitude, float distance) {
- float split_offset = (perc * 0.4);
- float split_point = 0.1 + split_offset;
-
- float amplitude_normal = smoothstep(split_point, 0.7, st.x);
- float amplitude_strength = 0.5;
- float finalAmplitude = amplitude_normal * amplitude_strength
- * amplitude * (1.0 + (mouse.y - 0.5) * 0.2);
-
- float time_scaled = time / 10.0 + (mouse.x - 0.5) * 1.0;
- float blur = smoothstep(split_point, split_point + 0.05, st.x) * perc;
-
- float xnoise = mix(
- Perlin2D(vec2(time_scaled, st.x + perc) * 2.5),
- Perlin2D(vec2(time_scaled, st.x + time_scaled) * 3.5) / 1.5,
- st.x * 0.3
- );
-
- float y = 0.5 + (perc - 0.5) * distance + xnoise / 2.0 * finalAmplitude;
-
- float line_start = smoothstep(
- y + (width / 2.0) + (u_line_blur * pixel(1.0, iResolution.xy) * blur),
- y,
- st.y
- );
-
- float line_end = smoothstep(
- y,
- y - (width / 2.0) - (u_line_blur * pixel(1.0, iResolution.xy) * blur),
- st.y
- );
-
- return clamp(
- (line_start - line_end) * (1.0 - smoothstep(0.0, 1.0, pow(perc, 0.3))),
- 0.0,
- 1.0
- );
-}
-
-void mainImage(out vec4 fragColor, in vec2 fragCoord) {
- vec2 uv = fragCoord / iResolution.xy;
-
- float line_strength = 1.0;
- for (int i = 0; i < u_line_count; i++) {
- float p = float(i) / float(u_line_count);
- line_strength *= (1.0 - lineFn(
- uv,
- u_line_width * pixel(1.0, iResolution.xy) * (1.0 - p),
- p,
- (PI * 1.0) * p,
- uMouse,
- iTime,
- uAmplitude,
- uDistance
- ));
- }
-
- float colorVal = 1.0 - line_strength;
- fragColor = vec4(uColor * colorVal, colorVal);
-}
-
-void main() {
- mainImage(gl_FragColor, gl_FragCoord.xy);
-}
-`;
-
-const Threads: React.FC = ({
- color = [1, 1, 1],
- amplitude = 1,
- distance = 0,
- enableMouseInteraction = false,
- ...rest
-}) => {
- const containerRef = useRef(null);
- const animationFrameId = useRef(null);
-
- useEffect(() => {
- if (!containerRef.current) return;
- const container = containerRef.current;
-
- const renderer = new Renderer({ alpha: true });
- const gl = renderer.gl;
- gl.clearColor(0, 0, 0, 0);
- gl.enable(gl.BLEND);
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
- container.appendChild(gl.canvas);
-
- const geometry = new Triangle(gl);
- const program = new Program(gl, {
- vertex: vertexShader,
- fragment: fragmentShader,
- uniforms: {
- iTime: { value: 0 },
- iResolution: {
- value: new Color(
- gl.canvas.width,
- gl.canvas.height,
- gl.canvas.width / gl.canvas.height
- ),
- },
- uColor: { value: new Color(...color) },
- uAmplitude: { value: amplitude },
- uDistance: { value: distance },
- uMouse: { value: new Float32Array([0.5, 0.5]) },
- },
- });
-
- const mesh = new Mesh(gl, { geometry, program });
-
- function resize() {
- const { clientWidth, clientHeight } = container;
- renderer.setSize(clientWidth, clientHeight);
- program.uniforms.iResolution.value.r = clientWidth;
- program.uniforms.iResolution.value.g = clientHeight;
- program.uniforms.iResolution.value.b = clientWidth / clientHeight;
- }
- window.addEventListener("resize", resize);
- resize();
-
- let currentMouse = [0.5, 0.5];
- let targetMouse = [0.5, 0.5];
-
- function handleMouseMove(e: MouseEvent) {
- const rect = container.getBoundingClientRect();
- const x = (e.clientX - rect.left) / rect.width;
- const y = 1.0 - (e.clientY - rect.top) / rect.height;
- targetMouse = [x, y];
- }
- function handleMouseLeave() {
- targetMouse = [0.5, 0.5];
- }
- if (enableMouseInteraction) {
- container.addEventListener("mousemove", handleMouseMove);
- container.addEventListener("mouseleave", handleMouseLeave);
- }
-
- function update(t: number) {
- if (enableMouseInteraction) {
- const smoothing = 0.05;
- currentMouse[0] += smoothing * (targetMouse[0] - currentMouse[0]);
- currentMouse[1] += smoothing * (targetMouse[1] - currentMouse[1]);
- program.uniforms.uMouse.value[0] = currentMouse[0];
- program.uniforms.uMouse.value[1] = currentMouse[1];
- } else {
- program.uniforms.uMouse.value[0] = 0.5;
- program.uniforms.uMouse.value[1] = 0.5;
- }
- program.uniforms.iTime.value = t * 0.001;
-
- renderer.render({ scene: mesh });
- animationFrameId.current = requestAnimationFrame(update);
- }
- animationFrameId.current = requestAnimationFrame(update);
-
- return () => {
- if (animationFrameId.current)
- cancelAnimationFrame(animationFrameId.current);
- window.removeEventListener("resize", resize);
-
- if (enableMouseInteraction) {
- container.removeEventListener("mousemove", handleMouseMove);
- container.removeEventListener("mouseleave", handleMouseLeave);
- }
- if (container.contains(gl.canvas)) container.removeChild(gl.canvas);
- gl.getExtension("WEBGL_lose_context")?.loseContext();
- };
- }, [color, amplitude, distance, enableMouseInteraction]);
-
- return (
-
- );
-};
-
-export default Threads;
diff --git a/LandingPage/src/components/card.tsx b/LandingPage/src/components/card.tsx
deleted file mode 100644
index 092fe6a..0000000
--- a/LandingPage/src/components/card.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React, { useRef, useState } from "react";
-
-interface Position {
- x: number;
- y: number;
-}
-
-interface SpotlightCardProps extends React.PropsWithChildren {
- className?: string;
- spotlightColor?: `rgba(${number}, ${number}, ${number}, ${number})`;
-}
-
-const SpotlightCard: React.FC = ({
- children,
- className = "",
- spotlightColor = "rgba(255, 255, 255, 0.25)"
-}) => {
- const divRef = useRef(null);
- const [isFocused, setIsFocused] = useState(false);
- const [position, setPosition] = useState({ x: 0, y: 0 });
- const [opacity, setOpacity] = useState(0);
-
- const handleMouseMove: React.MouseEventHandler = (e) => {
- if (!divRef.current || isFocused) return;
-
- const rect = divRef.current.getBoundingClientRect();
- setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
- };
-
- const handleFocus = () => {
- setIsFocused(true);
- setOpacity(0.6);
- };
-
- const handleBlur = () => {
- setIsFocused(false);
- setOpacity(0);
- };
-
- const handleMouseEnter = () => {
- setOpacity(0.6);
- };
-
- const handleMouseLeave = () => {
- setOpacity(0);
- };
-
- return (
-
- );
-};
-
-export default SpotlightCard;
\ No newline at end of file
diff --git a/LandingPage/src/components/carousel.tsx b/LandingPage/src/components/carousel.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/LandingPage/src/components/form.tsx b/LandingPage/src/components/form.tsx
deleted file mode 100644
index 16007d9..0000000
--- a/LandingPage/src/components/form.tsx
+++ /dev/null
@@ -1,221 +0,0 @@
-import { useState } from 'react';
-import { Drawer, Button, TextField } from '@mui/material';
-import { motion } from 'framer-motion';
-import SendButton from "../components/sendbutton";
-import WatchlistButton from "../components/watchlist";
-
-const chatbotSteps = {
- initial: {
- question: 'Are you a Brand or a Creator?',
- options: ['Brand', 'Creator'],
- },
- Brand: [
- { question: 'What is your brand name?', type: 'text', key: 'brandName' },
- { question: 'What industry are you in?', type: 'text', key: 'industry' },
- { question: 'What is your monthly ad budget?', type: 'text', key: 'budget' },
- { question: 'Do you work with influencers already?', type: 'text', key: 'influencerExperience' },
- { question: 'What platforms do you use for advertising?', type: 'text', key: 'adPlatforms' },
- { question: 'What is your target audience?', type: 'text', key: 'targetAudience' },
- { question: 'Do you have a campaign in mind already?', type: 'text', key: 'campaignDetails' },
- ],
- Creator: [
- { question: 'What is your name?', type: 'text', key: 'creatorName' },
- { question: 'What is your email?', type: 'text', key: 'email' },
- { question: 'What is your phone number?', type: 'text', key: 'phone' },
- { question: 'Which platform do you use the most?', type: 'text', key: 'platform' },
- { question: 'Provide your social media handle', type: 'text', key: 'socialMedia' },
- { question: 'How many subscribers do you have?', type: 'text', key: 'subscribers' },
- { question: 'How many followers do you have?', type: 'text', key: 'followers' },
- { question: 'What’s your niche or content type?', type: 'text', key: 'niche' },
- { question: 'Are you open to exclusive brand deals?', type: 'text', key: 'exclusiveDeals' },
- { question: 'Do you have a portfolio?', type: 'text', key: 'portfolio' },
- ],
-};
-
-
-export default function ChatbotSidebarForm() {
- const [open, setOpen] = useState(false);
- const [chat, setChat] = useState([{ from: 'bot', text: chatbotSteps.initial.question }]);
- const [stepIndex, setStepIndex] = useState(0);
- const [path, setPath] = useState(null);
- const [formData, setFormData] = useState<{ [key: string]: string }>({});
- const [userInput, setUserInput] = useState('');
- const [completed, setCompleted] = useState(false);
-
- const FORM_ID = '';
- const formUrl = `https://docs.google.com/forms/d/e/${FORM_ID}/formResponse`;
-
-
- const submitToGoogleForm = async () => {
-
- const formBody = new URLSearchParams();
- formBody.append('entry.1234567890', formData.brandName || formData.creatorName);
- formBody.append('entry.2345678901', formData.industry);
- formBody.append('entry.3456789012', formData.budget);
- formBody.append('entry.4567890123', formData.influencerExperience);
- formBody.append('entry.5678901234', formData.adPlatforms);
- formBody.append('entry.6789012345', formData.targetAudience);
- formBody.append('entry.7890123456', formData.campaignDetails);
- formBody.append('entry.8901234567', formData.creatorName);
- formBody.append('entry.9012345678', formData.email);
- formBody.append('entry.0123456789', formData.phone);
- formBody.append('entry.1234567890', formData.platform);
- formBody.append('entry.2345678901', formData.socialMedia);
- formBody.append('entry.3456789012', formData.subscribers);
- formBody.append('entry.4567890123', formData.followers);
- formBody.append('entry.5678901234', formData.niche);
- formBody.append('entry.6789012345', formData.exclusiveDeals);
- formBody.append('entry.7890123456', formData.portfolio);
-
-
- try {
- await fetch(formUrl, {
- method: 'POST',
- mode: 'no-cors',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: formBody.toString(),
- });
- console.log("Submitted to Google Form");
- } catch (err) {
- console.error("Failed to submit to Google Form", err);
- }
- };
-
- const handleOptionClick = (option: 'Brand' | 'Creator') => {
- setChat((prev) => [...prev, { from: 'user', text: option }]);
- setPath(option);
- setTimeout(() => {
- setChat((prev) => [
- ...prev,
- { from: 'bot', text: chatbotSteps[option][0].question },
- ]);
- setStepIndex(0);
- }, 500);
- };
-
- const handleNext = () => {
- const currentStep = chatbotSteps[path!][stepIndex];
- if (!userInput) return;
-
- setFormData((prev) => ({ ...prev, [currentStep.key]: userInput }));
- setChat((prev) => [...prev, { from: 'user', text: userInput }]);
- setUserInput('');
-
- if (stepIndex + 1 < chatbotSteps[path!].length) {
- const nextQuestion = chatbotSteps[path!][stepIndex + 1].question;
- setTimeout(() => {
- setChat((prev) => [...prev, { from: 'bot', text: nextQuestion }]);
- }, 500);
- setStepIndex(stepIndex + 1);
- } else {
- setTimeout(() => {
- setChat((prev) => [
- ...prev,
- { from: 'bot', text: 'Thanks for joining the waitlist! 🎉' },
- ]);
- setCompleted(true);
- submitToGoogleForm();
- setTimeout(() => setOpen(false), 2000); // auto close after 2 seconds
- }, 500);
- }
- };
-
- const handleDrawerOpen = () => {
- setOpen(true);
- setChat([{ from: 'bot', text: chatbotSteps.initial.question }]);
- setStepIndex(0);
- setPath(null);
- setFormData({});
- setUserInput('');
- setCompleted(false);
- };
-
- return (
-
-
-
-
-
-
setOpen(false)}>
-
-
- {/* Header */}
-
- 🤖 Automated Chat Form
-
-
- {/* Chat Body */}
-
- {chat.map((msg, idx) => (
-
- {msg.text}
-
- ))}
-
-
- {/* Input Section */}
-
- {!path ? (
-
- {chatbotSteps.initial.options.map((opt) => (
- handleOptionClick(opt as 'Brand' | 'Creator')}
- sx={{
- borderColor: '#8B5CF6',
- color: '#8B5CF6',
- fontWeight: 600,
- '&:hover': {
- backgroundColor: '#8B5CF6',
- color: '#fff',
- },
- }}
- >
- {opt}
-
- ))}
-
- ) : !completed && stepIndex < chatbotSteps[path].length ? (
-
-
setUserInput(e.target.value)}
- disabled={completed}
- placeholder="Type your answer..."
- sx={{
- input: { color: '#000' },
- backgroundColor: 'white',
- borderRadius: '8px',
- }}
- />
-
-
-
-
- ) : null}
-
-
-
-
-
- );
-}
diff --git a/LandingPage/src/components/github.tsx b/LandingPage/src/components/github.tsx
deleted file mode 100644
index a1a33ff..0000000
--- a/LandingPage/src/components/github.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import styled from 'styled-components';
-
-const Github = () => {
- return (
-
-
-
-
-
- Contribute on Github
-
-
- );
-}
-
-const StyledWrapper = styled.div`
- .btn-github {
- cursor: pointer;
- display: flex;
- gap: 0.5rem;
- border: none;
-
- transition: all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1);
- border-radius: 100px;
- font-weight: 800;
- place-content: center;
-
- padding: 0.75rem 1rem;
- font-size: 0.825rem;
- line-height: 1rem;
-
- background-color: rgba(255, 255, 255, 0.19);
- box-shadow:
- inset 0 1px 0 0 rgba(255, 255, 255, 0.04),
- inset 0 0 0 1px rgba(255, 255, 255, 0.04);
- color: #fff;
- }
-
- .btn-github:hover {
- box-shadow:
- inset 0 1px 0 0 rgba(255, 255, 255, 0.08),
- inset 0 0 0 1px rgba(252, 232, 3, 0.08);
- color:rgb(152, 3, 252);
- transform: translate(0, -0.25rem);
- background-color: rgba(0, 0, 0, 0.5);
- }`;
-
-export default Github;
diff --git a/LandingPage/src/components/howitworks.tsx b/LandingPage/src/components/howitworks.tsx
deleted file mode 100644
index 13e4e26..0000000
--- a/LandingPage/src/components/howitworks.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import React from 'react';
-import { motion } from 'framer-motion';
-import {
- UserIcon,
- SparklesIcon,
- CreditCardIcon,
- BarChartIcon,
- LayoutDashboardIcon,
- CameraIcon
-
-} from 'lucide-react';
-
-const HowItWorks: React.FC = () => {
-
- const steps = [
- {
- title: "User Onboarding",
- description: "Brands and creators register and choose their roles, preferences, and categories.",
- icon: ,
- color: "from-pink-500 to-purple-500"
- },
- {
- title: "AI-Powered Matching",
- description: "Inpact uses AI to suggest ideal brand-creator collaborations based on past work, niches, and engagement.",
- icon: ,
- color: "from-pink-500 to-purple-500"
- },
- {
- title: "Creator Showcases",
- description: "Creators can highlight their portfolios and previous collaborations, making it easier for brands to evaluate fit.",
- icon: ,
- color: "from-purple-500 to-pink-500"
- },
- {
- title: "Collaboration Dashboard",
- description: "Both parties interact, chat, and collaborate with full task and timeline visibility.",
- icon: ,
- color: "from-purple-500 to-pink-500"
- },
- {
- title: "Smart Contracts & Payments",
- description: "Secure agreements and transactions powered by Stripe or Razorpay integrations.",
- icon: ,
- color: "from-pink-500 to-purple-500"
- },
- {
- title: "Analytics & Feedback",
- description: "Track campaign metrics, gather insights, and iterate smarter with built-in dashboards.",
- icon: ,
- color: "from-pink-500 to-purple-500"
- }
- ];
-
-
-
- return (
-
-
-
-
-
- How InpactAI works
-
-
-
- Inpact uses AI-powered pipelines to bridge the gap between brands and creators—simplifying discovery, onboarding, and collaboration.
-
-
-
-
- {steps.map((step, index) => (
-
-
-
- {step.icon}
-
-
-
{step.title}
-
{step.description}
-
-
-
- {index < steps.length - 1 && (
- <>
-
- {step.title !== "Smart Contracts & Payments" &&
- step.title !== "Analytics & Feedback" &&
- (
-
- )}
-
-
-
- >
- )}
-
- ))}
-
-
-
-
-
-
-
- );
-};
-
-export default HowItWorks;
\ No newline at end of file
diff --git a/LandingPage/src/components/integration.tsx b/LandingPage/src/components/integration.tsx
deleted file mode 100644
index 1155635..0000000
--- a/LandingPage/src/components/integration.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { motion } from 'framer-motion';
-import { SocialIcon } from 'react-social-icons'
-
-export default function Integrations() {
- const integrations = [
- {
- icon: ,
- name: 'Instagram',
- description: 'Fetch creator insights like engagement rate, reach trends, and content breakdown.',
- },
- {
- icon: ,
- name: 'YouTube',
- description: 'Access analytics on video performance, channel growth, and viewer demographics.',
- },
- {
- icon: ,
- name: 'X (formerly Twitter)',
- description: 'Measure influence through tweet engagement, retweet rate, and follower insights.',
- },
- {
- icon: ,
- name: 'LinkedIn',
- description: 'Track professional creator presence and branded thought leadership impact.',
- },
- ];
-
- const container = {
- hidden: { opacity: 0 },
- show: {
- opacity: 1,
- transition: {
- staggerChildren: 0.12,
- delayChildren: 0.2,
- },
- },
- };
-
- const item = {
- hidden: { opacity: 0, y: 24 },
- show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: 'easeOut' } },
- };
-
- return (
-
-
-
-
- Social Integrations
-
-
-
- Inpact connects with major social platforms to analyze creator performance and brand-fit intelligence.
-
-
-
-
- {integrations.map((integration, index) => (
-
-
-
- {integration.icon}
-
-
{integration.name}
-
-
- {integration.description}
-
-
- ))}
-
-
-
- );
-}
diff --git a/LandingPage/src/components/sendbutton.tsx b/LandingPage/src/components/sendbutton.tsx
deleted file mode 100644
index 67019d8..0000000
--- a/LandingPage/src/components/sendbutton.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import styled from 'styled-components';
-
-const SendButton = () => {
- return (
-
-
-
- Send
-
-
- );
-}
-
-const StyledWrapper = styled.div`
- button {
- font-family: inherit;
- font-size: 14px;
- background:rgb(101, 0, 135);
- color: white;
- padding: 0.9em 1.2em;
- padding-left: 0.9em;
- display: flex;
- align-items: center;
- border: none;
- border-radius: 16px;
- overflow: hidden;
- transition: all 0.2s;
- cursor: pointer;
- }
-
- button span {
- display: block;
- margin-left: 0.3em;
- transition: all 0.3s ease-in-out;
- }
-
- button svg {
- display: block;
- transform-origin: center center;
- transition: transform 0.3s ease-in-out;
- }
-
- button:hover .svg-wrapper {
- animation: fly-1 0.6s ease-in-out infinite alternate;
- }
-
- button:hover svg {
- transform: translateX(1.2em) rotate(45deg) scale(1.1);
- }
-
- button:hover span {
- transform: translateX(5em);
- }
-
- button:active {
- transform: scale(0.95);
- }
-
- @keyframes fly-1 {
- from {
- transform: translateY(0.1em);
- }
-
- to {
- transform: translateY(-0.1em);
- }
- }`;
-
-export default SendButton;
diff --git a/LandingPage/src/components/watchlist.tsx b/LandingPage/src/components/watchlist.tsx
deleted file mode 100644
index 2e0d9de..0000000
--- a/LandingPage/src/components/watchlist.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import styled from 'styled-components';
-
-const WatchlistButton = () => {
- return (
-
-
- Join Watchlist
-
-
-
- );
-}
-
-const StyledWrapper = styled.div`
- .cssbuttons-io-button {
- background:rgb(115, 24, 252);
- color: white;
- font-family: inherit;
- padding: 0.35em;
- padding-left: 1.2em;
- font-size: 17px;
- font-weight: 500;
- border-radius: 0.9em;
- border: none;
- letter-spacing: 0.05em;
- display: flex;
- align-items: center;
- box-shadow: inset 0 0 1.6em -0.6em #714da6;
- overflow: hidden;
- position: relative;
- height: 2.8em;
- padding-right: 3.3em;
- cursor: pointer;
- }
-
- .cssbuttons-io-button .icon {
- background: white;
- margin-left: 1em;
- position: absolute;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 2.2em;
- width: 2.2em;
- border-radius: 0.7em;
- box-shadow: 0.1em 0.1em 0.6em 0.2em #7b52b9;
- right: 0.3em;
- transition: all 0.3s;
- }
-
- .cssbuttons-io-button:hover .icon {
- width: calc(100% - 0.6em);
- }
-
- .cssbuttons-io-button .icon svg {
- width: 1.1em;
- transition: transform 0.3s;
- color: #7b52b9;
- }
-
- .cssbuttons-io-button:hover .icon svg {
- transform: translateX(0.1em);
- }
-
- .cssbuttons-io-button:active .icon {
- transform: scale(0.95);
- }`;
-
-export default WatchlistButton;
diff --git a/LandingPage/src/index.css b/LandingPage/src/index.css
deleted file mode 100644
index ddd5849..0000000
--- a/LandingPage/src/index.css
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Applies to WebKit browsers */
-::-webkit-scrollbar {
- width: 8px;
- }
-
- ::-webkit-scrollbar-track {
- background: #000000;
- border-radius: 3px;
- }
-
- ::-webkit-scrollbar-thumb {
- background: linear-gradient(to bottom, #fc5fff, #764e95); /* Gradient colors */
- border-radius: 3px;
- }
-
- ::-webkit-scrollbar-thumb:hover {
- background: linear-gradient(to bottom, #de43e9, #764e95); /* Darker on hover */
- }
-
\ No newline at end of file
diff --git a/LandingPage/src/main.tsx b/LandingPage/src/main.tsx
deleted file mode 100644
index bef5202..0000000
--- a/LandingPage/src/main.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
-
-createRoot(document.getElementById('root')!).render(
-
-
- ,
-)
diff --git a/LandingPage/src/vite-env.d.ts b/LandingPage/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe..0000000
--- a/LandingPage/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/LandingPage/tsconfig.app.json b/LandingPage/tsconfig.app.json
deleted file mode 100644
index 358ca9b..0000000
--- a/LandingPage/tsconfig.app.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["src"]
-}
diff --git a/LandingPage/tsconfig.json b/LandingPage/tsconfig.json
deleted file mode 100644
index 1ffef60..0000000
--- a/LandingPage/tsconfig.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "files": [],
- "references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ]
-}
diff --git a/LandingPage/tsconfig.node.json b/LandingPage/tsconfig.node.json
deleted file mode 100644
index db0becc..0000000
--- a/LandingPage/tsconfig.node.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "target": "ES2022",
- "lib": ["ES2023"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/LandingPage/vite.config.ts b/LandingPage/vite.config.ts
deleted file mode 100644
index 8b0f57b..0000000
--- a/LandingPage/vite.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-
-// https://vite.dev/config/
-export default defineConfig({
- plugins: [react()],
-})
diff --git a/README.md b/README.md
index 07d283d..60786af 100644
--- a/README.md
+++ b/README.md
@@ -4,215 +4,654 @@
Inpact is an open-source AI-powered platform designed to connect content creators, brands, and agencies through data-driven insights. By leveraging Generative AI (GenAI), audience analytics, and engagement metrics, Inpact ensures highly relevant sponsorship opportunities for creators while maximizing ROI for brands investing in influencer marketing.
-## Features
+---
+
+## 📋 Table of Contents
+
+- [Features](#features)
+- [Tech Stack](#tech-stack)
+- [Project Structure](#project-structure)
+- [Prerequisites](#prerequisites)
+- [Installation & Setup](#installation--setup)
+ - [1. Clone the Repository](#1-clone-the-repository)
+ - [2. Frontend Setup](#2-frontend-setup)
+ - [3. Backend Setup](#3-backend-setup)
+ - [4. Database Setup](#4-database-setup)
+ - [5. Environment Variables](#5-environment-variables)
+- [Running the Application](#running-the-application)
+- [API Documentation](#api-documentation)
+- [Usage Guide](#usage-guide)
+- [Project Workflow](#project-workflow)
+- [Troubleshooting](#troubleshooting)
+- [Contributing](#contributing)
+- [Contact](#contact)
+
+---
+
+## ✨ Features
### AI-Driven Sponsorship Matchmaking
-- Automatically connects creators with brands based on audience demographics, engagement rates, and content style.
+- Automatically connects creators with brands based on audience demographics, engagement rates, and content style
+- Real-time matching algorithm powered by AI
+- Personalized recommendations for both creators and brands
### AI-Powered Creator Collaboration Hub
-- Facilitates partnerships between creators with complementary audiences and content niches.
+- Facilitates partnerships between creators with complementary audiences and content niches
+- AI-suggested collaboration ideas based on audience overlap
+- Joint campaign planning and execution tools
### AI-Based Pricing & Deal Optimization
-- Provides fair sponsorship pricing recommendations based on engagement, market trends, and historical data.
+- Provides fair sponsorship pricing recommendations based on engagement, market trends, and historical data
+- Dynamic pricing suggestions for different campaign types
+- Budget optimization for brands
### AI-Powered Negotiation & Contract Assistant
-- Assists in structuring deals, generating contracts, and optimizing terms using AI insights.
+- Assists in structuring deals, generating contracts, and optimizing terms using AI insights
+- Automated contract generation with customizable templates
+- Smart negotiation suggestions
### Performance Analytics & ROI Tracking
-- Enables brands and creators to track sponsorship performance, audience engagement, and campaign success.
+- Enables brands and creators to track sponsorship performance, audience engagement, and campaign success
+- Real-time analytics dashboards
+- AI-powered insights and recommendations for future campaigns
+- Screenshot extraction and metric tracking using Gemini Vision API
-## Tech Stack
+### Campaign Management
-- **Frontend**: ReactJS
-- **Backend**: FastAPI
-- **Database**: Supabase
-- **AI Integration**: GenAI for audience analysis and sponsorship recommendations
+- Full lifecycle campaign management for brands
+- Creator proposal system with status tracking
+- Deliverable tracking and metric submission
+- Contract management and signing workflow
---
-## Workflow
+## 🛠 Tech Stack
-### 1. User Registration & Profile Setup
+### Frontend
-- Creators, brands, and agencies sign up and set up their profiles.
-- AI gathers audience insights and engagement data.
+- **Framework**: Next.js 16.0.1 (React 19.2.0)
+- **Language**: TypeScript
+- **Styling**: Tailwind CSS 4
+- **State Management**: Zustand
+- **Data Fetching**: TanStack React Query, Axios
+- **Forms**: React Hook Form with Zod validation
+- **Animations**: Framer Motion
+- **Charts**: Recharts
+- **Authentication**: Supabase Auth
+- **Icons**: Lucide React
-### 2. AI-Powered Sponsorship Matchmaking
+### Backend
-- The platform suggests brands and sponsorship deals based on audience metrics.
-- Creators can apply for sponsorships or receive brand invitations.
+- **Framework**: FastAPI 0.120.3
+- **Language**: Python 3.13+
+- **Database**: Supabase (PostgreSQL)
+- **Authentication**: Supabase Auth with JWT
+- **AI Integration**:
+ - Groq API (for text generation)
+ - Google Gemini API (for content generation and vision tasks)
+- **Server**: Uvicorn
+- **Validation**: Pydantic
-### 3. Collaboration Hub
+### Database & Services
-- Creators can find and connect with others for joint campaigns.
-- AI recommends potential collaborations based on niche and audience overlap.
+- **Database**: Supabase PostgreSQL
+- **Authentication**: Supabase Auth
+- **Storage**: Supabase Storage (for images and files)
+- **Real-time**: Supabase Realtime (for live updates)
-### 4. AI-Based Pricing & Contract Optimization
+---
-- AI provides fair pricing recommendations for sponsorships.
-- Auto-generates contract templates with optimized terms.
+## 📁 Project Structure
-### 5. Campaign Execution & Tracking
+```
+InPactAI/
+├── frontend/ # Next.js frontend application
+│ ├── app/ # Next.js App Router
+│ │ ├── brand/ # Brand-specific pages
+│ │ │ ├── analytics/ # Analytics dashboard
+│ │ │ ├── campaigns/ # Campaign management
+│ │ │ ├── contracts/ # Contract management
+│ │ │ ├── home/ # Brand dashboard
+│ │ │ ├── onboarding/ # Brand onboarding flow
+│ │ │ └── proposals/ # Proposal management
+│ │ ├── creator/ # Creator-specific pages
+│ │ │ ├── analytics/ # Creator analytics
+│ │ │ ├── collaborations/ # Collaboration hub
+│ │ │ ├── contracts/ # Contract management
+│ │ │ ├── home/ # Creator dashboard
+│ │ │ ├── onboarding/ # Creator onboarding flow
+│ │ │ └── proposals/ # Proposal applications
+│ │ ├── login/ # Login page
+│ │ ├── signup/ # Signup page
+│ │ └── layout.tsx # Root layout
+│ ├── components/ # Reusable React components
+│ │ ├── analytics/ # Analytics components
+│ │ ├── auth/ # Authentication components
+│ │ ├── contracts/ # Contract components
+│ │ ├── dashboard/ # Dashboard components
+│ │ └── onboarding/ # Onboarding components
+│ ├── lib/ # Utility libraries
+│ │ ├── api/ # API client functions
+│ │ ├── auth-helpers.ts # Auth utilities
+│ │ ├── campaignApi.ts # Campaign API client
+│ │ ├── geminiApi.ts # Gemini API client
+│ │ └── supabaseClient.ts # Supabase client
+│ ├── types/ # TypeScript type definitions
+│ ├── public/ # Static assets
+│ ├── package.json # Frontend dependencies
+│ └── next.config.ts # Next.js configuration
+│
+├── backend/ # FastAPI backend application
+│ ├── app/ # Main application code
+│ │ ├── api/ # API routes
+│ │ │ └── routes/ # Route handlers
+│ │ │ ├── analytics.py # Analytics endpoints
+│ │ │ ├── auth.py # Authentication endpoints
+│ │ │ ├── campaigns.py # Campaign endpoints
+│ │ │ ├── collaborations.py # Collaboration endpoints
+│ │ │ ├── creators.py # Creator endpoints
+│ │ │ ├── gemini_generate.py # Gemini AI endpoints
+│ │ │ ├── groq_generate.py # Groq AI endpoints
+│ │ │ ├── health.py # Health check endpoints
+│ │ │ └── proposals.py # Proposal endpoints
+│ │ ├── core/ # Core application logic
+│ │ │ ├── config.py # Configuration settings
+│ │ │ ├── dependencies.py # FastAPI dependencies
+│ │ │ ├── security.py # Security utilities
+│ │ │ └── supabase_clients.py # Supabase clients
+│ │ ├── db/ # Database utilities
+│ │ ├── models/ # Pydantic models
+│ │ ├── services/ # Business logic services
+│ │ └── main.py # FastAPI application entry point
+│ ├── requirements.txt # Python dependencies
+│ └── env_example # Environment variables example
+│
+├── docs/ # Documentation
+│ └── database/ # Database schema documentation
+│
+├── guides/ # Implementation guides
+│ └── summaries/ # Feature implementation summaries
+│
+└── README.md # This file
+```
-- Creators execute sponsorship campaigns.
-- Brands track campaign performance through engagement and ROI metrics.
+---
-### 6. Performance Analysis & Continuous Optimization
+## 📋 Prerequisites
-- AI analyzes campaign success and suggests improvements for future deals.
-- Brands and creators receive insights for optimizing future sponsorships.
+Before you begin, ensure you have the following installed on your system:
----
+### Required Software
+
+- **Node.js** (v18 or higher) - [Download](https://nodejs.org/)
+- **npm** (comes with Node.js) or **yarn**
+- **Python** (3.11 or higher) - [Download](https://www.python.org/downloads/)
+- **pip** (Python package manager)
+- **Git** - [Download](https://git-scm.com/downloads)
-## Getting Started
+### Required Accounts & API Keys
-### Prerequisites
+- **Supabase Account** - [Sign up](https://supabase.com/)
+- **Groq API Key** (for AI features) - [Get API Key](https://console.groq.com/)
+- **Google Gemini API Key** (for AI features) - [Get API Key](https://makersuite.google.com/app/apikey)
-Ensure you have the following installed:
+### Recommended Tools
-- Node.js & npm
-- Python & FastAPI
-- Supabase account
+- **VS Code** or any modern code editor
+- **Postman** or **Insomnia** (for API testing)
+- **Supabase CLI** (optional, for local development)
-### Installation
+---
+
+## 🚀 Installation & Setup
-#### 1. Clone the repository
+### 1. Clone the Repository
-```sh
+```bash
git clone https://github.com/AOSSIE-Org/InPact.git
-cd inpact
+cd InPact
```
-#### 2. Frontend Setup
+### 2. Frontend Setup
-1. Navigate to the frontend directory:
-```sh
+#### Step 1: Navigate to Frontend Directory
+
+```bash
cd frontend
```
-2. Install dependencies:
-```sh
+#### Step 2: Install Dependencies
+
+```bash
npm install
```
+This will install all required packages listed in `package.json`.
-3. Create a `.env` file using `.env-example` file:
+#### Step 3: Create Environment File
+Create a `.env.local` file in the `frontend` directory:
+```bash
+touch .env.local
+```
+
+#### Step 4: Configure Environment Variables
+
+Add the following variables to `.env.local`:
+
+```env
+# Supabase Configuration
+NEXT_PUBLIC_SUPABASE_URL=your-supabase-project-url
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
+
+# Backend API URL
+NEXT_PUBLIC_API_URL=http://localhost:8000
+```
-4. Get your Supabase credentials:
- - Go to [Supabase](https://supabase.com/)
- - Log in and create a new project (or use existing)
- - Go to Project Settings -> API
- - Copy the "Project URL" and paste it as VITE_SUPABASE_URL
- - Copy the "anon public" key and paste it as VITE_SUPABASE_ANON_KEY
+**How to get Supabase credentials:**
-#### 3. Backend Setup
+1. Go to [Supabase Dashboard](https://app.supabase.com/)
+2. Log in and create a new project (or select an existing one)
+3. Navigate to **Project Settings** → **API**
+4. Copy the **Project URL** and paste it as `NEXT_PUBLIC_SUPABASE_URL`
+5. Copy the **anon public** key and paste it as `NEXT_PUBLIC_SUPABASE_ANON_KEY`
-1. Navigate to the backend directory:
-```sh
+> **Important**: For production, ensure `NEXT_PUBLIC_API_URL` uses HTTPS.
+
+### 3. Backend Setup
+
+#### Step 1: Navigate to Backend Directory
+
+```bash
cd ../backend
```
-2. Install dependencies:
-```sh
+#### Step 2: Create Virtual Environment (Recommended)
+
+```bash
+# Create virtual environment
+python -m venv venv
+
+# Activate virtual environment
+# On macOS/Linux:
+source venv/bin/activate
+# On Windows:
+# venv\Scripts\activate
+```
+
+#### Step 3: Install Dependencies
+
+```bash
pip install -r requirements.txt
```
+#### Step 4: Navigate to App Directory
-3. Navigate to the app directory:
-```sh
+```bash
cd app
```
-4. Create a `.env` file using `.env-example` as a reference.
+#### Step 5: Create Environment File
-5. Obtain Supabase credentials:
+Create a `.env` file in the `backend/app` directory:
- - Go to [Supabase](https://supabase.com/)
- - Log in and create a new project
- - Click on the project and remember the project password
- - Go to the **Connect** section at the top
- - Select **SQLAlchemy** and copy the connection string:
+```bash
+touch .env
+```
- ```sh
- user=postgres
- password=[YOUR-PASSWORD]
- host=db.wveftanaurduixkyijhf.supabase.co
- port=5432
- dbname=postgres
- ```
+#### Step 6: Configure Environment Variables
- --OR--
+Copy the example file and edit it:
- [The above works in ipv6 networks, if you are in ipv4 network or it cause errors, use the below connection string which could be found in Session Pooler connection]
+```bash
+cp ../env_example .env
+```
- ```sh
- user=postgres.
- password=[YOUR-PASSWORD]
- host=aws-.pooler.supabase.com
- port=5432
- dbname=postgres
- ```
+Edit `.env` with your credentials:
+```env
+# Supabase Configuration (Required)
+SUPABASE_URL=https://your-project.supabase.co
+SUPABASE_KEY=your-supabase-anon-key-here
+SUPABASE_SERVICE_KEY=your-service-role-key
-6. Get the Groq API key:
- - Visit [Groq Console](https://console.groq.com/)
- - Create an API key and paste it into the `.env` file
+# Database Configuration (Optional - for direct PostgreSQL connection)
+DATABASE_URL=postgresql://postgres.your-project-ref:[YOUR-PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres
-#### 4. Start Development Servers
+# AI Configuration (Optional)
+GROQ_API_KEY=your-groq-api-key
+GEMINI_API_KEY=your-gemini-api-key-here
+# CORS Origins (comma-separated)
+ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
-1. Start the frontend server (from the frontend directory):
-```sh
-npm run dev
+# JWT Secret Key from Supabase
+# Location: Dashboard → Project Settings → API → JWT Settings → JWT Secret
+SUPABASE_JWT_SECRET=your-jwt-secret-from-supabase-dashboard
```
-2. Start the backend server (from the backend/app directory):
-```sh
-uvicorn main:app --reload
-```
+**How to get Supabase credentials:**
+
+1. **Supabase URL & Keys:**
+ - Go to **Project Settings** → **API**
+ - Copy **Project URL** → `SUPABASE_URL`
+ - Copy **anon public** key → `SUPABASE_KEY`
+ - Copy **service_role** key → `SUPABASE_SERVICE_KEY` (keep this secret!)
+
+2. **Database URL (Optional):**
+ - Go to **Project Settings** → **Database**
+ - Under **Connection string**, select **URI** or **Session Pooler**
+ - For IPv6 networks, use the direct connection:
+ ```
+ postgresql://postgres:[YOUR-PASSWORD]@db.xxxxx.supabase.co:5432/postgres
+ ```
+ - For IPv4 networks or if you encounter connection issues, use the Session Pooler:
+ ```
+ postgresql://postgres.xxxxx:[YOUR-PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres
+ ```
+
+3. **JWT Secret:**
+ - Go to **Project Settings** → **API** → **JWT Settings**
+ - Copy the **JWT Secret** (NOT the anon key!)
+
+4. **AI API Keys:**
+ - **Groq**: Visit [Groq Console](https://console.groq.com/) → Create API key
+ - **Gemini**: Visit [Google AI Studio](https://makersuite.google.com/app/apikey) → Create API key
-## Data Population
+### 4. Database Setup
-To populate the database with initial data, follow these steps:
+#### Step 1: Set Up Database Schema
1. **Open Supabase Dashboard**
+ - Go to [Supabase Dashboard](https://app.supabase.com/)
+ - Select your project
- - Go to [Supabase](https://supabase.com/) and log in.
- - Select your created project.
+2. **Access SQL Editor**
+ - In the left sidebar, click on **SQL Editor**
-2. **Access the SQL Editor**
+3. **Run Schema Script**
+ - The database schema is located in `backend/SQL`
+ - Copy the contents of the SQL file
+ - Paste into the SQL Editor
+ - Click **Run** to execute
- - In the left sidebar, click on **SQL Editor**.
+ > **Note**: The schema file contains table definitions and ENUM types. Make sure to run it in the correct order.
-3. **Run the SQL Script**
- - Open the `sql.txt` file in your project.
- - Copy the SQL queries from the file.
- - Paste the queries into the SQL Editor and click **Run**.
+#### Step 2: Verify Tables
-This will populate the database with the required initial data for the platform. 🚀
+After running the schema, verify that the following tables exist:
+
+- `profiles`
+- `brands`
+- `creators`
+- `campaigns`
+- `proposals`
+- `contracts`
+- `collaborations`
+- `campaign_deliverables`
+- `campaign_metrics`
+- And other related tables
+
+#### Step 3: Add Onboarding Column (if needed)
+
+If the `profiles` table doesn't have the `onboarding_completed` column, run:
+
+```sql
+ALTER TABLE profiles
+ADD COLUMN IF NOT EXISTS onboarding_completed BOOLEAN DEFAULT FALSE;
+```
+
+### 5. Environment Variables
+
+#### Frontend Environment Variables (`.env.local`)
+
+| Variable | Description | Required | Example |
+| ------------------------------- | ------------------------ | -------- | --------------------------- |
+| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL | Yes | `https://xxxxx.supabase.co` |
+| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase anon/public key | Yes | `eyJhbGc...` |
+| `NEXT_PUBLIC_API_URL` | Backend API URL | Yes | `http://localhost:8000` |
+
+#### Backend Environment Variables (`.env`)
+
+| Variable | Description | Required | Example |
+| ---------------------- | ---------------------------- | -------- | --------------------------- |
+| `SUPABASE_URL` | Supabase project URL | Yes | `https://xxxxx.supabase.co` |
+| `SUPABASE_KEY` | Supabase anon key | Yes | `eyJhbGc...` |
+| `SUPABASE_SERVICE_KEY` | Supabase service role key | Yes | `eyJhbGc...` |
+| `SUPABASE_JWT_SECRET` | JWT secret from Supabase | Yes | `your-jwt-secret` |
+| `DATABASE_URL` | PostgreSQL connection string | No | `postgresql://...` |
+| `GROQ_API_KEY` | Groq API key for AI | No | `gsk_xxxxx` |
+| `GEMINI_API_KEY` | Gemini API key for AI | No | `AIzaSy...` |
+| `ALLOWED_ORIGINS` | CORS allowed origins | No | `http://localhost:3000` |
---
-## Contributing
+## 🏃 Running the Application
+
+### Development Mode
+
+#### Start Backend Server
+
+1. Navigate to the backend app directory:
+
+ ```bash
+ cd backend/app
+ ```
+
+2. Activate virtual environment (if using one):
+
+ ```bash
+ source ../venv/bin/activate # macOS/Linux
+ # or
+ venv\Scripts\activate # Windows
+ ```
+
+3. Start the FastAPI server:
+
+ ```bash
+ uvicorn main:app --reload
+ ```
+
+ The backend will be available at: `http://localhost:8000`
+ - API Documentation: `http://localhost:8000/docs` (Swagger UI)
+ - Alternative Docs: `http://localhost:8000/redoc` (ReDoc)
+
+#### Start Frontend Server
+
+1. Navigate to the frontend directory:
+
+ ```bash
+ cd frontend
+ ```
+
+2. Start the Next.js development server:
+ ```bash
+ npm run dev
+ ```
+
+````
+
+ The frontend will be available at: `http://localhost:3000`
+
+### Production Build
+
+#### Build Frontend
+
+```bash
+cd frontend
+npm run build
+npm start
+````
+
+#### Build Backend
+
+```bash
+cd backend/app
+uvicorn main:app --host 0.0.0.0 --port 8000
+```
+
+---
+
+## 📚 API Documentation
+
+### Base URL
+
+- **Development**: `http://localhost:8000`
+- **Production**: Your production backend URL
+
+### Main API Endpoints
+
+#### Authentication
+
+- `POST /api/auth/signup` - User registration
+- `POST /api/auth/login` - User login
+- `GET /api/auth/me` - Get current user profile
+
+#### Campaigns
+
+- `GET /campaigns` - List campaigns (with filters)
+- `POST /campaigns` - Create new campaign
+- `GET /campaigns/{campaign_id}` - Get campaign details
+- `PUT /campaigns/{campaign_id}` - Update campaign
+- `DELETE /campaigns/{campaign_id}` - Delete campaign
-We welcome contributions from the community! To contribute:
+#### Proposals
-1. Fork the repository.
-2. Create a new branch for your feature (`git checkout -b feature-name`).
-3. Commit your changes (`git commit -m "Added feature"`).
-4. Push to your branch (`git push origin feature-name`).
-5. Open a Pull Request.
+- `GET /proposals` - List proposals
+- `POST /proposals` - Create proposal
+- `GET /proposals/{proposal_id}` - Get proposal details
+- `PUT /proposals/{proposal_id}` - Update proposal
+- `DELETE /proposals/{proposal_id}` - Delete proposal
+- `POST /proposals/{proposal_id}/accept` - Accept proposal
+- `POST /proposals/{proposal_id}/reject` - Reject proposal
+
+#### Contracts
+
+- `GET /contracts` - List contracts
+- `GET /contracts/{contract_id}` - Get contract details
+- `POST /contracts` - Create contract
+- `PUT /contracts/{contract_id}` - Update contract
+
+#### Collaborations
+
+- `GET /collaborations` - List collaborations
+- `POST /collaborations` - Create collaboration
+- `GET /collaborations/{collaboration_id}` - Get collaboration details
+
+#### Creators
+
+- `GET /creators` - List creators (with search/filters)
+- `GET /creators/{creator_id}` - Get creator profile
+
+#### Analytics
+
+- `GET /analytics/campaigns/{campaign_id}` - Get campaign analytics
+- `POST /analytics/metrics` - Create metric
+- `POST /analytics/metrics/{metric_id}/submit` - Submit metric value
+- `POST /analytics/screenshots/extract` - Extract metrics from screenshot
+
+#### AI Generation
+
+- `POST /api/groq/generate` - Generate content using Groq
+- `POST /api/gemini/generate` - Generate content using Gemini
+
+#### Health Check
+
+- `GET /health` - Health check endpoint
+- `GET /health/supabase` - Supabase connection check
+
+### Interactive API Documentation
+
+Once the backend is running, visit:
+
+- **Swagger UI**: `http://localhost:8000/docs`
+- **ReDoc**: `http://localhost:8000/redoc`
+
+These provide interactive documentation where you can test endpoints directly.
---
-## Overall Workflow
+## 📖 Usage Guide
+
+### For Creators
+
+1. **Sign Up / Login**
+ - Visit the signup page and create an account
+ - Select "Creator" as your role
+ - Verify your email address
+
+2. **Complete Onboarding**
+ - Fill out your profile information
+ - Add your social media platforms
+ - Specify your niche and content types
+ - Upload profile picture
+
+3. **Browse Campaigns**
+ - View available sponsorship opportunities
+ - Filter by niche, budget, platform, etc.
+ - View campaign details and requirements
+
+4. **Apply for Sponsorships**
+ - Submit proposals for campaigns
+ - Include your pitch and portfolio
+ - Track application status
+
+5. **Manage Collaborations**
+ - Discover potential collaboration partners
+ - Send collaboration proposals
+ - Manage active collaborations
+
+6. **Track Performance**
+ - View analytics for your campaigns
+ - Submit deliverables and metrics
+ - Monitor engagement and ROI
+
+### For Brands
+
+1. **Sign Up / Login**
+ - Create a brand account
+ - Select "Brand" as your role
+ - Verify your email
+
+2. **Complete Onboarding**
+ - Add company information
+ - Define target audience
+ - Set marketing goals and budget
+ - Upload brand assets
+
+3. **Create Campaigns**
+ - Create new sponsorship campaigns
+ - Define deliverables and requirements
+ - Set budget and timeline
+ - Specify preferred creator criteria
+
+4. **Review Proposals**
+ - View creator applications
+ - Review creator profiles and portfolios
+ - Accept or reject proposals
+
+5. **Manage Contracts**
+ - Generate contracts using AI
+ - Negotiate terms
+ - Track contract status
+
+6. **Monitor Analytics**
+ - View campaign performance
+ - Track ROI and engagement metrics
+ - Analyze creator submissions
+ - Get AI-powered insights
+
+---
+
+## 🔄 Project Workflow
+
+### Overall System Workflow
```mermaid
graph TD;
@@ -227,7 +666,7 @@ graph TD;
I -->|Feedback Loop| C;
```
-**FRONTEND workflow in detail**
+### Frontend Workflow
```mermaid
graph TD;
@@ -243,7 +682,7 @@ graph TD;
J -->|Show Performance Analytics| K[AI Optimizes Future Matches];
```
-**BACKEND workflow in detail**
+### Backend Workflow
```mermaid
graph TD;
@@ -262,10 +701,195 @@ graph TD;
M -->|Return Insights| N[AI Refines Future Recommendations];
```
-## Contact
+---
+
+## 🔧 Troubleshooting
+
+### Common Issues
+
+#### Frontend Issues
+
+**Issue: "Missing Supabase environment variables"**
+
+- **Solution**: Ensure `.env.local` exists in the `frontend` directory with all required variables
+- Check that variable names start with `NEXT_PUBLIC_` for client-side access
+
+**Issue: "NEXT_PUBLIC_API_URL is missing or not valid"**
+
+- **Solution**: For local development, set `NEXT_PUBLIC_API_URL=http://localhost:8000`
+- For production, ensure it's a valid HTTPS URL
+
+**Issue: Build fails with TypeScript errors**
+
+- **Solution**: Run `npm install` to ensure all dependencies are installed
+- Check that TypeScript version is compatible (v5+)
+
+#### Backend Issues
+
+**Issue: "Supabase client initialization failed"**
+
+- **Solution**:
+ - Verify `SUPABASE_URL` and `SUPABASE_KEY` in `.env`
+ - Check that your Supabase project is active
+ - Ensure network connectivity to Supabase
+
+**Issue: "JWT verification failed"**
+
+- **Solution**:
+ - Verify `SUPABASE_JWT_SECRET` matches the JWT Secret in Supabase dashboard
+ - Ensure you're using the JWT Secret, not the anon key
+
+**Issue: Database connection errors**
+
+- **Solution**:
+ - Check `DATABASE_URL` format (if using direct connection)
+ - For IPv4 networks, use Session Pooler connection string
+ - Verify database password is correct
+ - Check Supabase project status
+
+**Issue: "Module not found" errors**
+
+- **Solution**:
+ - Activate virtual environment: `source venv/bin/activate`
+ - Reinstall dependencies: `pip install -r requirements.txt`
+
+#### Database Issues
+
+**Issue: Tables don't exist**
+
+- **Solution**: Run the SQL schema script in Supabase SQL Editor
+- Check that all ENUM types are created before tables
+
+**Issue: "onboarding_completed column doesn't exist"**
-For queries, issues, or feature requests, please raise an issue or reach out on our Discord server.
+- **Solution**: Run the ALTER TABLE command:
+ ```sql
+ ALTER TABLE profiles
+ ADD COLUMN IF NOT EXISTS onboarding_completed BOOLEAN DEFAULT FALSE;
+ ```
+#### API Issues
-Happy Coding!
+**Issue: CORS errors**
+
+- **Solution**:
+ - Add frontend URL to `ALLOWED_ORIGINS` in backend `.env`
+ - Ensure backend CORS middleware is configured correctly
+
+**Issue: 401 Unauthorized errors**
+
+- **Solution**:
+ - Verify JWT token is being sent in request headers
+ - Check token expiration
+ - Ensure user is logged in
+
+### Getting Help
+
+If you encounter issues not covered here:
+
+1. Check the [Issues](https://github.com/AOSSIE-Org/InPact/issues) page
+2. Review the documentation in the `docs/` and `guides/` directories
+3. Reach out on Discord or create a new issue
+
+---
+
+## 🤝 Contributing
+
+We welcome contributions from the community! Here's how you can help:
+
+### Contribution Process
+
+1. **Fork the Repository on GitHub**
+ - Go to [https://github.com/AOSSIE-Org/InPact](https://github.com/AOSSIE-Org/InPact) and click the "Fork" button in the top right.
+
+2. **Clone Your Fork Locally**
+
+ ```bash
+ git clone
+ cd InPact
+ ```
+
+3. **Add the Original Repository as Upstream Remote**
+
+ ```bash
+ git remote add upstream https://github.com/AOSSIE-Org/InPact.git
+ ```
+
+4. **Create a Feature Branch**
+
+ ```bash
+ git checkout -b feature/your-feature-name
+ ```
+
+5. **Make Your Changes**
+ - Write clean, documented code
+ - Follow existing code style
+ - Add tests if applicable
+ - Update documentation as needed
+
+6. **Commit Your Changes**
+
+ ```bash
+ git commit -m "Add: Description of your feature"
+ ```
+
+ Use clear, descriptive commit messages:
+ - `Add:` for new features
+ - `Fix:` for bug fixes
+ - `Update:` for updates to existing features
+ - `Docs:` for documentation changes
+
+7. **Push to Your Fork**
+
+ ```bash
+ git push origin feature/your-feature-name
+ ```
+
+8. **Open a Pull Request**
+ - Go to the repository on GitHub
+ - Click "New Pull Request"
+ - Select your branch
+ - Fill out the PR template
+ - Submit for review
+
+### Code Style Guidelines
+
+- **Frontend**: Follow Next.js and React best practices
+- **Backend**: Follow PEP 8 Python style guide
+- **TypeScript**: Use strict mode, avoid `any` types
+- **Comments**: Add comments for complex logic
+- **Documentation**: Update README and code comments
+
+### Development Setup for Contributors
+
+1. Follow the installation steps above
+2. Create a separate branch for your work
+3. Test your changes thoroughly
+4. Ensure all tests pass (if applicable)
+5. Update relevant documentation
+
+---
+
+## 📞 Contact
+
+- **GitHub Issues**: [Create an issue](https://github.com/AOSSIE-Org/InPact/issues)
+- **Discord**: Join our Discord server (link in repository)
+- **Email**: Contact through GitHub profile
+
+---
+
+## 📄 License
+
+This project is open source. Please check the LICENSE file for details.
+
+---
+
+## 🙏 Acknowledgments
+
+- Built with ❤️ by the AOSSIE community
+- Powered by Supabase, Next.js, and FastAPI
+- AI capabilities powered by Groq and Google Gemini
+
+---
+**Happy Coding! 🚀**
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644
index 0000000..5dfbe6f
--- /dev/null
+++ b/backend/.env.example
@@ -0,0 +1,25 @@
+# Example environment file for backend
+# Application Settings
+APP_NAME=InPactAI
+
+GEMINI_API_KEY=your-gemini-api-key-here
+# Supabase Configuration
+SUPABASE_URL=https://yoursupabaseurl.supabase.co
+SUPABASE_KEY=your-supabase-anon-key-here
+SUPABASE_SERVICE_KEY=your-service-role-key
+
+
+# Database Configuration (Supabase PostgreSQL)
+# Get this from: Settings → Database → Connection string → URI
+DATABASE_URL=postgresql://postgres.your-project-ref:[YOUR-PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres
+
+# AI Configuration
+GROQ_API_KEY=your-groq-api-key
+AI_API_KEY=your-openai-api-key-optional
+
+# CORS Origins (comma-separated)
+ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
+
+# Server Configuration
+HOST=0.0.0.0
+PORT=8000
diff --git a/backend/API_ENDPOINTS.md b/backend/API_ENDPOINTS.md
new file mode 100644
index 0000000..36189b2
--- /dev/null
+++ b/backend/API_ENDPOINTS.md
@@ -0,0 +1,159 @@
+# InPact AI - API Endpoints Reference
+
+**Total Endpoints: 109**
+
+## Quick Access to API Documentation
+
+FastAPI automatically generates interactive API documentation:
+
+- **Swagger UI**: `http://localhost:8000/docs` (or your production URL + `/docs`)
+- **ReDoc**: `http://localhost:8000/redoc` (or your production URL + `/redoc`)
+- **OpenAPI JSON**: `http://localhost:8000/openapi.json`
+
+## Endpoint Categories
+
+### Authentication (`/api/auth`)
+- `POST /api/auth/signup` - User registration
+- `POST /api/auth/login` - User login
+
+### Health Checks (`/health`)
+- `GET /health/` - Basic health check
+- `GET /health/supabase` - Supabase connection check
+
+### Campaigns (`/campaigns`)
+- `GET /campaigns` - List campaigns
+- `GET /campaigns/public` - Public campaigns
+- `GET /campaigns/{campaign_id}` - Get campaign details
+- `POST /campaigns` - Create campaign
+- `PUT /campaigns/{campaign_id}` - Update campaign
+- `DELETE /campaigns/{campaign_id}` - Delete campaign
+- `GET /campaigns/{campaign_id}/deliverables` - Get deliverables
+- `GET /campaigns/{campaign_id}/find-creators` - Find matching creators
+- `GET /campaigns/{campaign_id}/search-creator` - Search creator
+- `GET /campaigns/{campaign_id}/applications` - Get applications
+- `POST /campaigns/{campaign_id}/applications` - Submit application
+- `PUT /campaigns/{campaign_id}/applications/{application_id}/status` - Update application status
+- `POST /campaigns/{campaign_id}/applications/{application_id}/create-proposal` - Create proposal from application
+
+### Proposals (`/proposals`)
+- `GET /proposals/sent` - Get sent proposals
+- `GET /proposals/received` - Get received proposals
+- `GET /proposals/negotiations` - Get negotiations
+- `GET /proposals/draft` - Draft proposal
+- `POST /proposals` - Create proposal
+- `PUT /proposals/{proposal_id}/status` - Update proposal status
+- `DELETE /proposals/{proposal_id}` - Delete proposal
+- `POST /proposals/{proposal_id}/negotiation/start` - Start negotiation
+- `POST /proposals/{proposal_id}/negotiation/messages` - Send negotiation message
+- `GET /proposals/{proposal_id}/negotiation/deal-probability` - Get deal probability
+- `POST /proposals/{proposal_id}/negotiation/analyze-sentiment` - Analyze sentiment
+- `POST /proposals/{proposal_id}/negotiation/draft-message` - Draft message
+- `POST /proposals/{proposal_id}/negotiation/translate` - Translate message
+- `POST /proposals/{proposal_id}/negotiation/accept` - Accept negotiation
+- `PUT /proposals/{proposal_id}/negotiation/terms` - Update negotiation terms
+
+### Analytics (`/analytics`)
+- `POST /analytics/metrics` - Create metric
+- `GET /analytics/metrics/{metric_id}` - Get metric
+- `PUT /analytics/metrics/{metric_id}` - Update metric
+- `DELETE /analytics/metrics/{metric_id}` - Delete metric
+- `GET /analytics/metrics/{metric_id}/history` - Get metric history
+- `POST /analytics/metrics/{metric_id}/submit` - Submit metric value
+- `POST /analytics/metrics/{metric_id}/extract-from-screenshot` - Extract from screenshot
+- `GET /analytics/campaigns/{campaign_id}/dashboard` - Campaign dashboard
+- `GET /analytics/brand/all-deliverables` - All brand deliverables
+- `GET /analytics/brand/dashboard-stats` - Brand dashboard stats
+- `GET /analytics/creator/campaigns` - Creator campaigns
+- `GET /analytics/creator/campaigns/{campaign_id}` - Creator campaign details
+- `GET /analytics/creator/dashboard-stats` - Creator dashboard stats
+- `GET /analytics/creator/deliverables/{deliverable_id}/metrics` - Deliverable metrics
+- `GET /analytics/creator/pending-requests` - Pending update requests
+- `POST /analytics/creator/metrics/{metric_id}/comment` - Add comment
+- `POST /analytics/metric-updates/{update_id}/feedback` - Add feedback
+- `POST /analytics/update-requests` - Create update request
+
+### AI Analytics (`/analytics/ai`)
+- `POST /analytics/ai/predictive` - Predictive analytics
+- `GET /analytics/ai/insights` - Automated insights
+- `GET /analytics/ai/audience-segmentation` - Audience segmentation
+- `POST /analytics/ai/sentiment` - Sentiment analysis
+- `GET /analytics/ai/anomaly-detection` - Anomaly detection
+- `GET /analytics/ai/attribution` - Attribution modeling
+- `GET /analytics/ai/benchmarking` - Benchmarking
+- `GET /analytics/ai/churn-prediction` - Churn prediction
+- `POST /analytics/ai/natural-language-query` - Natural language query
+- `GET /analytics/ai/kpi-optimization` - KPI optimization
+
+### Profiles (`/brand/profile`, `/creator/profile`)
+- `GET /brand/profile` - Get brand profile
+- `PUT /brand/profile` - Update brand profile
+- `POST /brand/profile/ai-fill` - AI fill brand profile
+- `GET /creator/profile` - Get creator profile
+- `PUT /creator/profile` - Update creator profile
+- `POST /creator/profile/ai-fill` - AI fill creator profile
+
+### Creators (`/creators`)
+- `GET /creators` - List creators
+- `GET /creators/recommendations` - Get recommendations
+- `GET /creators/{creator_id}` - Get creator details
+- `GET /creators/niches/list` - List niches
+- `GET /creators/campaign-wall/recommendations` - Campaign wall recommendations
+- `GET /creators/applications` - Creator applications
+
+### Collaborations (`/collaborations`)
+- `GET /collaborations` - List collaborations
+- `GET /collaborations/{collaboration_id}` - Get collaboration
+- `GET /collaborations/{collaboration_id}/workspace` - Get workspace
+- `GET /collaborations/stats/summary` - Get stats summary
+- `POST /collaborations/propose` - Propose collaboration
+- `POST /collaborations/generate-ideas` - Generate ideas
+- `POST /collaborations/recommend-creator` - Recommend creator
+- `POST /collaborations/{collaboration_id}/accept` - Accept collaboration
+- `POST /collaborations/{collaboration_id}/decline` - Decline collaboration
+- `POST /collaborations/{collaboration_id}/complete` - Complete collaboration
+- `POST /collaborations/{collaboration_id}/deliverables` - Create deliverable
+- `PATCH /collaborations/{collaboration_id}/deliverables/{deliverable_id}` - Update deliverable
+- `POST /collaborations/{collaboration_id}/messages` - Send message
+- `POST /collaborations/{collaboration_id}/assets` - Upload asset
+- `POST /collaborations/{collaboration_id}/feedback` - Submit feedback
+
+### Contracts (`/contracts`)
+- `GET /contracts` - List contracts
+- `GET /contracts/{contract_id}` - Get contract
+- `GET /contracts/{contract_id}/deliverables` - Get deliverables
+- `GET /contracts/{contract_id}/versions` - Get versions
+- `GET /contracts/{contract_id}/versions/current` - Get current version
+- `GET /contracts/{contract_id}/chat` - Get chat
+- `GET /contracts/{contract_id}/summarize` - Summarize contract
+- `POST /contracts/generate-template` - Generate template
+- `POST /contracts/{contract_id}/ask-question` - Ask question
+- `POST /contracts/{contract_id}/translate` - Translate contract
+- `POST /contracts/{contract_id}/explain-clause` - Explain clause
+- `POST /contracts/{contract_id}/deliverables` - Create deliverable
+- `POST /contracts/{contract_id}/deliverables/approve` - Approve deliverables
+- `POST /contracts/{contract_id}/deliverables/{deliverable_id}/submit` - Submit deliverable
+- `POST /contracts/{contract_id}/deliverables/{deliverable_id}/review` - Review deliverable
+- `POST /contracts/{contract_id}/request-status-change` - Request status change
+- `POST /contracts/{contract_id}/respond-status-change` - Respond to status change
+- `POST /contracts/{contract_id}/track-signed-download` - Track signed download
+- `POST /contracts/{contract_id}/track-unsigned-download` - Track unsigned download
+- `POST /contracts/{contract_id}/versions` - Create version
+- `POST /contracts/{contract_id}/versions/{version_id}/approve` - Approve version
+- `PUT /contracts/{contract_id}/signed-link` - Update signed link
+- `PUT /contracts/{contract_id}/unsigned-link` - Update unsigned link
+
+### AI Generation (`/generate`, `/groq/generate`)
+- `POST /generate` - Gemini generation
+- `POST /groq/generate` - Groq generation
+
+## How to Use
+
+1. **Interactive Documentation**: Visit `/docs` when your backend is running
+2. **List Endpoints**: Run `python3 backend/list_endpoints.py`
+3. **Test Endpoints**: Use the Swagger UI at `/docs` or tools like Postman/curl
+
+## Base URL
+
+- **Local**: `http://localhost:8000`
+- **Production**: Set via `NEXT_PUBLIC_API_URL` environment variable
+
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000..666559c
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,3 @@
+# Backend
+
+Project backend structure and setup instructions.
diff --git a/backend/SQL b/backend/SQL
new file mode 100644
index 0000000..3261396
--- /dev/null
+++ b/backend/SQL
@@ -0,0 +1,773 @@
+-- Define custom ENUM types (required by several tables below)
+DO $$ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'application_status') THEN
+ CREATE TYPE application_status AS ENUM ('applied', 'reviewing', 'accepted', 'rejected');
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'invite_status') THEN
+ CREATE TYPE invite_status AS ENUM ('pending', 'accepted', 'declined', 'expired');
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'payment_status') THEN
+ CREATE TYPE payment_status AS ENUM ('pending', 'processing', 'completed', 'failed', 'refunded');
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'deal_status') THEN
+ CREATE TYPE deal_status AS ENUM ('draft', 'proposed', 'negotiating', 'active', 'completed', 'cancelled');
+ END IF;
+END $$;
+
+
+-- This file is no longer maintained here.
+-- For the latest schema reference and documentation, see: docs/database/schema-reference.md
+
+CREATE TABLE public.brands (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ user_id uuid NOT NULL UNIQUE,
+ company_name text NOT NULL,
+ company_tagline text,
+ company_description text,
+ company_logo_url text,
+ company_cover_image_url text,
+ industry text NOT NULL,
+ sub_industry text[] DEFAULT ARRAY[]::text[],
+ company_size text,
+ founded_year integer,
+ headquarters_location text,
+ company_type text,
+ website_url text NOT NULL,
+ contact_email text,
+ contact_phone text,
+ social_media_links jsonb,
+ target_audience_age_groups text[] DEFAULT ARRAY[]::text[],
+ target_audience_gender text[] DEFAULT ARRAY[]::text[],
+ target_audience_locations text[] DEFAULT ARRAY[]::text[],
+ target_audience_interests text[] DEFAULT ARRAY[]::text[],
+ target_audience_income_level text[] DEFAULT ARRAY[]::text[],
+ target_audience_description text,
+ brand_values text[] DEFAULT ARRAY[]::text[],
+ brand_personality text[] DEFAULT ARRAY[]::text[],
+ brand_voice text,
+ brand_colors jsonb,
+ marketing_goals text[] DEFAULT ARRAY[]::text[],
+ campaign_types_interested text[] DEFAULT ARRAY[]::text[],
+ preferred_content_types text[] DEFAULT ARRAY[]::text[],
+ preferred_platforms text[] DEFAULT ARRAY[]::text[],
+ campaign_frequency text,
+ monthly_marketing_budget numeric,
+ influencer_budget_percentage double precision,
+ budget_per_campaign_min numeric,
+ budget_per_campaign_max numeric,
+ typical_deal_size numeric,
+ payment_terms text,
+ offers_product_only_deals boolean DEFAULT false,
+ offers_affiliate_programs boolean DEFAULT false,
+ affiliate_commission_rate double precision,
+ preferred_creator_niches text[] DEFAULT ARRAY[]::text[],
+ preferred_creator_size text[] DEFAULT ARRAY[]::text[],
+ preferred_creator_locations text[] DEFAULT ARRAY[]::text[],
+ minimum_followers_required integer,
+ minimum_engagement_rate double precision,
+ content_dos text[] DEFAULT ARRAY[]::text[],
+ content_donts text[] DEFAULT ARRAY[]::text[],
+ brand_safety_requirements text[] DEFAULT ARRAY[]::text[],
+ competitor_brands text[] DEFAULT ARRAY[]::text[],
+ exclusivity_required boolean DEFAULT false,
+ exclusivity_duration_months integer,
+ past_campaigns_count integer DEFAULT 0,
+ successful_partnerships text[] DEFAULT ARRAY[]::text[],
+ case_studies text[] DEFAULT ARRAY[]::text[],
+ average_campaign_roi double precision,
+ products_services text[] DEFAULT ARRAY[]::text[],
+ product_price_range text,
+ product_categories text[] DEFAULT ARRAY[]::text[],
+ seasonal_products boolean DEFAULT false,
+ product_catalog_url text,
+ business_verified boolean DEFAULT false,
+ payment_verified boolean DEFAULT false,
+ tax_id_verified boolean DEFAULT false,
+ profile_completion_percentage integer DEFAULT 0,
+ is_active boolean DEFAULT true,
+ is_featured boolean DEFAULT false,
+ is_verified_brand boolean DEFAULT false,
+ subscription_tier text DEFAULT 'free'::text,
+ featured_until timestamp with time zone,
+ ai_profile_summary text,
+ search_keywords text[] DEFAULT ARRAY[]::text[],
+ matching_score_base double precision DEFAULT 50.0,
+ total_deals_posted integer DEFAULT 0,
+ total_deals_completed integer DEFAULT 0,
+ total_spent numeric DEFAULT 0,
+ average_deal_rating double precision,
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ last_active_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT brands_pkey PRIMARY KEY (id),
+ CONSTRAINT brands_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id)
+);
+CREATE TABLE public.campaign_applications (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ campaign_id uuid NOT NULL,
+ creator_id uuid NOT NULL,
+ profile_snapshot jsonb DEFAULT '{}'::jsonb,
+ message text,
+ proposed_amount numeric,
+ attachments jsonb DEFAULT '[]'::jsonb,
+ status USER-DEFINED DEFAULT 'applied'::application_status,
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT campaign_applications_pkey PRIMARY KEY (id),
+ CONSTRAINT campaign_applications_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id),
+ CONSTRAINT campaign_applications_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id)
+);
+CREATE TABLE public.campaign_assets (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ campaign_id uuid,
+ deal_id uuid,
+ uploaded_by uuid,
+ url text NOT NULL,
+ type text,
+ meta jsonb DEFAULT '{}'::jsonb,
+ created_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT campaign_assets_pkey PRIMARY KEY (id),
+ CONSTRAINT campaign_assets_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id),
+ CONSTRAINT campaign_assets_deal_id_fkey FOREIGN KEY (deal_id) REFERENCES public.deals(id),
+ CONSTRAINT campaign_assets_uploaded_by_fkey FOREIGN KEY (uploaded_by) REFERENCES public.profiles(id)
+);
+CREATE TABLE public.campaign_deliverables (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ campaign_id uuid NOT NULL,
+ platform text,
+ content_type text,
+ quantity integer DEFAULT 1,
+ guidance text,
+ required boolean DEFAULT true,
+ created_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT campaign_deliverables_pkey PRIMARY KEY (id),
+ CONSTRAINT campaign_deliverables_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id)
+);
+CREATE TABLE public.campaign_invites (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ campaign_id uuid NOT NULL,
+ brand_id uuid NOT NULL,
+ creator_id uuid NOT NULL,
+ message text,
+ proposed_amount numeric,
+ status USER-DEFINED DEFAULT 'pending'::invite_status,
+ sent_at timestamp with time zone,
+ responded_at timestamp with time zone,
+ created_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT campaign_invites_pkey PRIMARY KEY (id),
+ CONSTRAINT campaign_invites_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id),
+ CONSTRAINT campaign_invites_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id),
+ CONSTRAINT campaign_invites_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id)
+);
+CREATE TABLE public.campaign_payments (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ deal_id uuid NOT NULL,
+ amount numeric NOT NULL,
+ currency text DEFAULT 'INR'::text,
+ method text,
+ status USER-DEFINED DEFAULT 'pending'::payment_status,
+ external_payment_ref text,
+ metadata jsonb DEFAULT '{}'::jsonb,
+ created_at timestamp with time zone DEFAULT now(),
+ paid_at timestamp with time zone,
+ CONSTRAINT campaign_payments_pkey PRIMARY KEY (id),
+ CONSTRAINT campaign_payments_deal_id_fkey FOREIGN KEY (deal_id) REFERENCES public.deals(id)
+);
+CREATE TABLE public.campaign_performance (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ campaign_id uuid NOT NULL,
+ deal_id uuid,
+ report_source text,
+ recorded_at timestamp with time zone NOT NULL DEFAULT now(),
+ impressions bigint,
+ clicks bigint,
+ views bigint,
+ watch_time bigint,
+ engagements bigint,
+ conversions bigint,
+ revenue numeric,
+ raw jsonb DEFAULT '{}'::jsonb,
+ CONSTRAINT campaign_performance_pkey PRIMARY KEY (id),
+ CONSTRAINT campaign_performance_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id),
+ CONSTRAINT campaign_performance_deal_id_fkey FOREIGN KEY (deal_id) REFERENCES public.deals(id)
+);
+CREATE TABLE public.campaigns (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ brand_id uuid NOT NULL,
+ title text NOT NULL,
+ slug text UNIQUE,
+ short_description text,
+ description text,
+ status text NOT NULL DEFAULT 'draft'::text,
+ platforms text[] DEFAULT ARRAY[]::text[],
+ deliverables jsonb DEFAULT '[]'::jsonb,
+ target_audience jsonb DEFAULT '{}'::jsonb,
+ budget_min numeric,
+ budget_max numeric,
+ preferred_creator_niches text[] DEFAULT ARRAY[]::text[],
+ preferred_creator_followers_range text,
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ published_at timestamp with time zone,
+ starts_at timestamp with time zone,
+ ends_at timestamp with time zone,
+ is_featured boolean DEFAULT false,
+ CONSTRAINT campaigns_pkey PRIMARY KEY (id),
+ CONSTRAINT campaigns_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id)
+);
+CREATE TABLE public.creators (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ user_id uuid NOT NULL UNIQUE,
+ display_name text NOT NULL,
+ bio text,
+ tagline text,
+ profile_picture_url text,
+ cover_image_url text,
+ website_url text,
+ youtube_url text,
+ youtube_handle text,
+ youtube_subscribers integer,
+ instagram_url text,
+ instagram_handle text,
+ instagram_followers integer,
+ tiktok_url text,
+ tiktok_handle text,
+ tiktok_followers integer,
+ twitter_url text,
+ twitter_handle text,
+ twitter_followers integer,
+ twitch_url text,
+ twitch_handle text,
+ twitch_followers integer,
+ linkedin_url text,
+ facebook_url text,
+ primary_niche text NOT NULL,
+ secondary_niches text[] DEFAULT ARRAY[]::text[],
+ content_types text[] DEFAULT ARRAY[]::text[],
+ content_language text[] DEFAULT ARRAY[]::text[],
+ total_followers integer DEFAULT 0,
+ total_reach integer,
+ average_views integer,
+ engagement_rate double precision,
+ audience_age_primary text,
+ audience_age_secondary text[] DEFAULT ARRAY[]::text[],
+ audience_gender_split jsonb,
+ audience_locations jsonb,
+ audience_interests text[] DEFAULT ARRAY[]::text[],
+ average_engagement_per_post integer,
+ posting_frequency text,
+ best_performing_content_type text,
+ peak_posting_times jsonb,
+ years_of_experience integer,
+ content_creation_full_time boolean DEFAULT false,
+ team_size integer DEFAULT 1,
+ equipment_quality text,
+ editing_software text[] DEFAULT ARRAY[]::text[],
+ collaboration_types text[] DEFAULT ARRAY[]::text[],
+ preferred_brands_style text[] DEFAULT ARRAY[]::text[],
+ not_interested_in text[] DEFAULT ARRAY[]::text[],
+ rate_per_post numeric,
+ rate_per_video numeric,
+ rate_per_story numeric,
+ rate_per_reel numeric,
+ rate_negotiable boolean DEFAULT true,
+ accepts_product_only_deals boolean DEFAULT false,
+ minimum_deal_value numeric,
+ preferred_payment_terms text,
+ portfolio_links text[] DEFAULT ARRAY[]::text[],
+ past_brand_collaborations text[] DEFAULT ARRAY[]::text[],
+ case_study_links text[] DEFAULT ARRAY[]::text[],
+ media_kit_url text,
+ email_verified boolean DEFAULT false,
+ phone_verified boolean DEFAULT false,
+ identity_verified boolean DEFAULT false,
+ profile_completion_percentage integer DEFAULT 0,
+ is_active boolean DEFAULT true,
+ is_featured boolean DEFAULT false,
+ is_verified_creator boolean DEFAULT false,
+ featured_until timestamp with time zone,
+ ai_profile_summary text,
+ search_keywords text[] DEFAULT ARRAY[]::text[],
+ matching_score_base double precision DEFAULT 50.0,
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ last_active_at timestamp with time zone DEFAULT now(),
+ social_platforms jsonb,
+ CONSTRAINT creators_pkey PRIMARY KEY (id),
+ CONSTRAINT creators_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id)
+);
+CREATE TABLE public.deals (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ campaign_id uuid,
+ application_id uuid,
+ brand_id uuid NOT NULL,
+ creator_id uuid NOT NULL,
+ agreed_amount numeric,
+ payment_schedule jsonb DEFAULT '[]'::jsonb,
+ terms jsonb DEFAULT '{}'::jsonb,
+ status USER-DEFINED DEFAULT 'draft'::deal_status,
+ starts_at timestamp with time zone,
+ ends_at timestamp with time zone,
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT deals_pkey PRIMARY KEY (id),
+ CONSTRAINT deals_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id),
+ CONSTRAINT deals_application_id_fkey FOREIGN KEY (application_id) REFERENCES public.campaign_applications(id),
+ CONSTRAINT deals_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id),
+ CONSTRAINT deals_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id)
+);
+CREATE TABLE public.match_scores (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ creator_id uuid NOT NULL,
+ brand_id uuid NOT NULL,
+ score double precision NOT NULL,
+ reasons jsonb DEFAULT '[]'::jsonb,
+ computed_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT match_scores_pkey PRIMARY KEY (id),
+ CONSTRAINT match_scores_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id),
+ CONSTRAINT match_scores_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id)
+);
+CREATE TABLE public.profiles (
+ id uuid NOT NULL,
+ name text NOT NULL,
+ role text NOT NULL CHECK (role = ANY (ARRAY['Creator'::text, 'Brand'::text])),
+ created_at timestamp with time zone DEFAULT timezone('utc'::text, now()),
+ onboarding_completed boolean DEFAULT false,
+ CONSTRAINT profiles_pkey PRIMARY KEY (id),
+ CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id)
+
+-- collaboration tables
+);
+CREATE TABLE IF NOT EXISTS creator_collaborations (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+
+ -- Creators involved
+ creator1_id UUID NOT NULL REFERENCES creators(id),
+ creator2_id UUID NOT NULL REFERENCES creators(id),
+
+ -- Collaboration Details
+ collaboration_type TEXT NOT NULL,
+ -- 'content_collab', 'cross_promotion', 'giveaway',
+ -- 'brand_package', 'series', 'other'
+
+ title TEXT NOT NULL,
+ description TEXT,
+
+ -- AI Match Data
+ match_score FLOAT,
+ ai_suggestions JSONB,
+ -- {
+ -- "collaboration_ideas": [...],
+ -- "strengths": [...],
+ -- "potential_challenges": [...]
+ -- }
+
+ -- Status & Timeline
+ status TEXT DEFAULT 'proposed',
+ -- 'proposed', 'accepted', 'planning', 'active',
+ -- 'completed', 'declined', 'cancelled'
+
+ start_date DATE,
+ end_date DATE,
+
+ -- Content Details
+ planned_deliverables JSONB,
+ -- {
+ -- "content_type": "video",
+ -- "platform": "youtube",
+ -- "quantity": 2,
+ -- "schedule": [...]
+ -- }
+
+ completed_deliverables JSONB[],
+
+ -- Communication
+ initiator_id UUID REFERENCES creators(id),
+ proposal_message TEXT,
+ response_message TEXT,
+
+ -- Performance Tracking
+ total_views INTEGER DEFAULT 0,
+ total_engagement INTEGER DEFAULT 0,
+ audience_growth JSONB,
+ -- {
+ -- "creator1_new_followers": 450,
+ -- "creator2_new_followers": 520
+ -- }
+
+ -- Ratings (after completion)
+ creator1_rating INTEGER, -- 1-5
+ creator1_feedback TEXT,
+ creator2_rating INTEGER,
+ creator2_feedback TEXT,
+
+ -- Timestamps
+ proposed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ accepted_at TIMESTAMP WITH TIME ZONE,
+ completed_at TIMESTAMP WITH TIME ZONE,
+
+ UNIQUE(creator1_id, creator2_id, title),
+ CHECK (creator1_id != creator2_id)
+);
+
+-- Collaboration match suggestions cache
+CREATE TABLE IF NOT EXISTS collaboration_suggestions (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ creator_id UUID NOT NULL REFERENCES creators(id),
+ suggested_creator_id UUID NOT NULL REFERENCES creators(id),
+
+ match_score FLOAT NOT NULL,
+ reasons JSONB,
+ collaboration_ideas JSONB,
+
+ calculated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ expires_at TIMESTAMP WITH TIME ZONE DEFAULT (NOW() + INTERVAL '7 days'),
+
+ UNIQUE(creator_id, suggested_creator_id)
+);
+
+-- Collaboration deliverables table
+CREATE TABLE IF NOT EXISTS collaboration_deliverables (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ collaboration_id UUID NOT NULL REFERENCES creator_collaborations(id) ON DELETE CASCADE,
+
+ title TEXT NOT NULL,
+ description TEXT,
+ content_type TEXT, -- 'video', 'post', 'story', 'reel', etc.
+ platform TEXT, -- 'youtube', 'instagram', 'tiktok', etc.
+ quantity INTEGER DEFAULT 1,
+ due_date DATE,
+
+ status TEXT DEFAULT 'pending', -- 'pending', 'in_progress', 'completed', 'approved'
+ assigned_to UUID REFERENCES creators(id), -- which creator is responsible
+
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ completed_at TIMESTAMP WITH TIME ZONE
+);
+
+-- Collaboration messages table (chat)
+CREATE TABLE IF NOT EXISTS collaboration_messages (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ collaboration_id UUID NOT NULL REFERENCES creator_collaborations(id) ON DELETE CASCADE,
+ sender_id UUID NOT NULL REFERENCES creators(id),
+
+ message TEXT NOT NULL,
+ message_type TEXT DEFAULT 'text', -- 'text', 'system', 'file_shared'
+
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+
+ -- For file sharing
+ attachment_url TEXT,
+ attachment_type TEXT -- 'image', 'video', 'document', 'link'
+);
+
+-- Collaboration assets table (shared files/URLs)
+CREATE TABLE IF NOT EXISTS collaboration_assets (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ collaboration_id UUID NOT NULL REFERENCES creator_collaborations(id) ON DELETE CASCADE,
+ uploaded_by UUID NOT NULL REFERENCES creators(id),
+
+ url TEXT NOT NULL,
+ asset_type TEXT, -- 'image', 'video', 'document', 'link', 'other'
+ title TEXT,
+ description TEXT,
+
+ -- Optional: link to a deliverable
+ deliverable_id UUID REFERENCES collaboration_deliverables(id),
+
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Indexes for performance
+CREATE INDEX IF NOT EXISTS idx_collaboration_deliverables_collab_id ON collaboration_deliverables(collaboration_id);
+CREATE INDEX IF NOT EXISTS idx_collaboration_deliverables_status ON collaboration_deliverables(status);
+CREATE INDEX IF NOT EXISTS idx_collaboration_messages_collab_id ON collaboration_messages(collaboration_id);
+CREATE INDEX IF NOT EXISTS idx_collaboration_messages_created_at ON collaboration_messages(created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_collaboration_assets_collab_id ON collaboration_assets(collaboration_id);
+CREATE INDEX IF NOT EXISTS idx_collaboration_assets_uploaded_by ON collaboration_assets(uploaded_by);
+
+CREATE TABLE IF NOT EXISTS public.proposals (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ campaign_id uuid NOT NULL,
+ brand_id uuid NOT NULL,
+ creator_id uuid NOT NULL,
+ subject text NOT NULL,
+ message text NOT NULL,
+ proposed_amount numeric,
+ content_ideas text[] DEFAULT ARRAY[]::text[],
+ ideal_pricing text,
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined', 'withdrawn')),
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ negotiation_status text DEFAULT 'none', -- 'none', 'open', 'finalized', 'declined'
+ negotiation_thread jsonb DEFAULT '[]'::jsonb, -- array of { sender_id, sender_role, message, timestamp }
+ current_terms jsonb DEFAULT '{}'::jsonb, -- latest terms snapshot
+ version integer DEFAULT 1,
+ contract_id uuid NULL,
+ CONSTRAINT proposals_pkey PRIMARY KEY (id),
+ CONSTRAINT proposals_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id) ON DELETE CASCADE,
+ CONSTRAINT proposals_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id) ON DELETE CASCADE,
+ CONSTRAINT proposals_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id) ON DELETE CASCADE,
+ CONSTRAINT proposals_unique_campaign_creator UNIQUE (campaign_id, creator_id, brand_id)
+);
+-- Contracts table for finalized deals
+CREATE TABLE IF NOT EXISTS public.contracts (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ proposal_id uuid NOT NULL REFERENCES public.proposals(id) ON DELETE CASCADE,
+ brand_id uuid NOT NULL REFERENCES public.brands(id) ON DELETE CASCADE,
+ creator_id uuid NOT NULL REFERENCES public.creators(id) ON DELETE CASCADE,
+ terms jsonb NOT NULL, -- snapshot of finalized terms
+ status text NOT NULL DEFAULT 'awaiting_signature', -- 'awaiting_signature', 'signed', 'active', 'completed', 'cancelled'
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now()
+);
+
+-- Indexes for contracts table
+CREATE INDEX IF NOT EXISTS idx_contracts_brand_id ON public.contracts(brand_id);
+CREATE INDEX IF NOT EXISTS idx_contracts_creator_id ON public.contracts(creator_id);
+CREATE INDEX IF NOT EXISTS idx_contracts_status ON public.contracts(status);
+
+-- Add updated_at trigger for contracts
+CREATE OR REPLACE FUNCTION update_contracts_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER update_contracts_updated_at
+ BEFORE UPDATE ON public.contracts
+ FOR EACH ROW
+ EXECUTE FUNCTION update_contracts_updated_at();
+
+-- Create indexes for proposals table
+CREATE INDEX IF NOT EXISTS idx_proposals_campaign_id ON public.proposals(campaign_id);
+CREATE INDEX IF NOT EXISTS idx_proposals_brand_id ON public.proposals(brand_id);
+CREATE INDEX IF NOT EXISTS idx_proposals_creator_id ON public.proposals(creator_id);
+CREATE INDEX IF NOT EXISTS idx_proposals_status ON public.proposals(status);
+CREATE INDEX IF NOT EXISTS idx_proposals_created_at ON public.proposals(created_at DESC);
+
+-- Add updated_at trigger for proposals
+CREATE OR REPLACE FUNCTION update_proposals_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER update_proposals_updated_at
+ BEFORE UPDATE ON public.proposals
+ FOR EACH ROW
+ EXECUTE FUNCTION update_proposals_updated_at();
+
+
+CREATE TABLE IF NOT EXISTS public.proposals (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ campaign_id uuid NOT NULL,
+ brand_id uuid NOT NULL,
+ creator_id uuid NOT NULL,
+ subject text NOT NULL,
+ message text NOT NULL,
+ proposed_amount numeric,
+ content_ideas text[] DEFAULT ARRAY[]::text[],
+ ideal_pricing text,
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined', 'withdrawn')),
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT proposals_pkey PRIMARY KEY (id),
+ CONSTRAINT proposals_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id) ON DELETE CASCADE,
+ CONSTRAINT proposals_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id) ON DELETE CASCADE,
+ CONSTRAINT proposals_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id) ON DELETE CASCADE,
+ CONSTRAINT proposals_unique_campaign_creator UNIQUE (campaign_id, creator_id, brand_id)
+);
+
+-- Indexes
+CREATE INDEX IF NOT EXISTS idx_proposals_campaign_id ON public.proposals(campaign_id);
+CREATE INDEX IF NOT EXISTS idx_proposals_brand_id ON public.proposals(brand_id);
+CREATE INDEX IF NOT EXISTS idx_proposals_creator_id ON public.proposals(creator_id);
+CREATE INDEX IF NOT EXISTS idx_proposals_status ON public.proposals(status);
+CREATE INDEX IF NOT EXISTS idx_proposals_created_at ON public.proposals(created_at DESC);
+
+-- Trigger for updated_at
+CREATE OR REPLACE FUNCTION update_proposals_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER update_proposals_updated_at
+ BEFORE UPDATE ON public.proposals
+ FOR EACH ROW
+ EXECUTE FUNCTION update_proposals_updated_at();
+
+
+-- Campaign Performance Analytics & ROI Tracking Tables
+
+-- 1. Table: campaign_deliverable_metrics
+-- Defines which metrics are tracked for each deliverable (including custom metrics)
+CREATE TABLE IF NOT EXISTS public.campaign_deliverable_metrics (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ campaign_deliverable_id uuid NOT NULL REFERENCES public.campaign_deliverables(id) ON DELETE CASCADE,
+ name text NOT NULL, -- e.g., 'impressions', 'likes', 'comments', 'custom_metric'
+ display_name text, -- For UI display, e.g., 'Total Likes'
+ target_value numeric, -- Brand's estimate/goal for this metric (nullable)
+ is_custom boolean DEFAULT false,
+ created_at timestamp with time zone DEFAULT now()
+);
+
+-- 2. Table: campaign_deliverable_metric_updates
+-- Stores actual metric values reported by the creator (manual or AI-extracted)
+CREATE TABLE IF NOT EXISTS public.campaign_deliverable_metric_updates (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ campaign_deliverable_metric_id uuid NOT NULL REFERENCES public.campaign_deliverable_metrics(id) ON DELETE CASCADE,
+ value numeric,
+ demographics jsonb, -- For audience demographics if applicable
+ screenshot_url text, -- For proof upload (optional)
+ ai_extracted_data jsonb, -- Raw AI/OCR output (optional)
+ submitted_by uuid NOT NULL REFERENCES public.profiles(id),
+ submitted_at timestamp with time zone DEFAULT now()
+);
+
+-- 3. Table: campaign_deliverable_metric_feedback
+-- Stores brand feedback on each metric update
+CREATE TABLE IF NOT EXISTS public.campaign_deliverable_metric_feedback (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ metric_update_id uuid NOT NULL REFERENCES public.campaign_deliverable_metric_updates(id) ON DELETE CASCADE,
+ brand_id uuid NOT NULL REFERENCES public.brands(id) ON DELETE CASCADE,
+ feedback_text text,
+ created_at timestamp with time zone DEFAULT now()
+);
+
+-- 4. Table: campaign_deliverable_metric_update_requests
+-- Tracks when a brand requests an update for a metric (or all metrics)
+CREATE TABLE IF NOT EXISTS public.campaign_deliverable_metric_update_requests (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ campaign_deliverable_metric_id uuid NOT NULL REFERENCES public.campaign_deliverable_metrics(id) ON DELETE CASCADE,
+ brand_id uuid NOT NULL REFERENCES public.brands(id) ON DELETE CASCADE,
+ creator_id uuid NOT NULL REFERENCES public.creators(id) ON DELETE CASCADE,
+ requested_at timestamp with time zone DEFAULT now(),
+ status text DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'cancelled'))
+);
+
+-- 5. Table: campaign_deliverable_metric_audit
+-- Keeps a history of all changes/updates for transparency (versioning)
+CREATE TABLE IF NOT EXISTS public.campaign_deliverable_metric_audit (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ campaign_deliverable_metric_id uuid NOT NULL REFERENCES public.campaign_deliverable_metrics(id) ON DELETE CASCADE,
+ old_value numeric,
+ new_value numeric,
+ changed_by uuid NOT NULL REFERENCES public.profiles(id),
+ changed_at timestamp with time zone DEFAULT now(),
+ change_reason text
+);
+
+-- Indexes for performance
+CREATE INDEX IF NOT EXISTS idx_campaign_deliverable_metrics_deliverable_id ON public.campaign_deliverable_metrics(campaign_deliverable_id);
+CREATE INDEX IF NOT EXISTS idx_metric_updates_metric_id ON public.campaign_deliverable_metric_updates(campaign_deliverable_metric_id);
+CREATE INDEX IF NOT EXISTS idx_metric_updates_submitted_by ON public.campaign_deliverable_metric_updates(submitted_by);
+CREATE INDEX IF NOT EXISTS idx_metric_feedback_update_id ON public.campaign_deliverable_metric_feedback(metric_update_id);
+CREATE INDEX IF NOT EXISTS idx_metric_feedback_brand_id ON public.campaign_deliverable_metric_feedback(brand_id);
+CREATE INDEX IF NOT EXISTS idx_metric_update_requests_metric_id ON public.campaign_deliverable_metric_update_requests(campaign_deliverable_metric_id);
+CREATE INDEX IF NOT EXISTS idx_metric_update_requests_brand_id ON public.campaign_deliverable_metric_update_requests(brand_id);
+CREATE INDEX IF NOT EXISTS idx_metric_update_requests_creator_id ON public.campaign_deliverable_metric_update_requests(creator_id);
+CREATE INDEX IF NOT EXISTS idx_metric_update_requests_status ON public.campaign_deliverable_metric_update_requests(status);
+CREATE INDEX IF NOT EXISTS idx_metric_audit_metric_id ON public.campaign_deliverable_metric_audit(campaign_deliverable_metric_id);
+
+
+
+-- Migration SQL for Campaign Wall Feature
+-- Run this SQL in your Supabase SQL editor
+
+-- 1. Add new columns to campaigns table for campaign wall feature
+ALTER TABLE public.campaigns
+ADD COLUMN IF NOT EXISTS is_open_for_applications boolean DEFAULT false,
+ADD COLUMN IF NOT EXISTS is_on_campaign_wall boolean DEFAULT false;
+
+-- 2. Add indexes for better query performance
+CREATE INDEX IF NOT EXISTS idx_campaigns_is_open_for_applications ON public.campaigns(is_open_for_applications) WHERE is_open_for_applications = true;
+CREATE INDEX IF NOT EXISTS idx_campaigns_is_on_campaign_wall ON public.campaigns(is_on_campaign_wall) WHERE is_on_campaign_wall = true;
+CREATE INDEX IF NOT EXISTS idx_campaigns_open_and_wall ON public.campaigns(is_open_for_applications, is_on_campaign_wall) WHERE is_open_for_applications = true AND is_on_campaign_wall = true;
+
+-- 3. Add new columns to campaign_applications table
+ALTER TABLE public.campaign_applications
+ADD COLUMN IF NOT EXISTS payment_min numeric,
+ADD COLUMN IF NOT EXISTS payment_max numeric,
+ADD COLUMN IF NOT EXISTS timeline_days integer,
+ADD COLUMN IF NOT EXISTS timeline_weeks integer,
+ADD COLUMN IF NOT EXISTS description text;
+
+-- 4. Add index for campaign_applications status filtering
+CREATE INDEX IF NOT EXISTS idx_campaign_applications_status ON public.campaign_applications(status);
+CREATE INDEX IF NOT EXISTS idx_campaign_applications_campaign_status ON public.campaign_applications(campaign_id, status);
+CREATE INDEX IF NOT EXISTS idx_campaign_applications_creator_status ON public.campaign_applications(creator_id, status);
+
+-- 5. Add comment for documentation
+COMMENT ON COLUMN public.campaigns.is_open_for_applications IS 'Whether this campaign accepts applications from creators';
+COMMENT ON COLUMN public.campaigns.is_on_campaign_wall IS 'Whether this campaign is visible on the public campaign wall';
+COMMENT ON COLUMN public.campaign_applications.payment_min IS 'Minimum payment amount the creator is requesting';
+COMMENT ON COLUMN public.campaign_applications.payment_max IS 'Maximum payment amount the creator is requesting';
+COMMENT ON COLUMN public.campaign_applications.timeline_days IS 'Number of days the creator estimates to complete the campaign';
+COMMENT ON COLUMN public.campaign_applications.timeline_weeks IS 'Number of weeks the creator estimates to complete the campaign';
+COMMENT ON COLUMN public.campaign_applications.description IS 'Creator description explaining why they should be chosen for this campaign';
+
+-- Migration SQL for Campaign Wall Feature
+-- Run this SQL in your Supabase SQL editor
+
+-- 1. Add new columns to campaigns table for campaign wall feature
+ALTER TABLE public.campaigns
+ADD COLUMN IF NOT EXISTS is_open_for_applications boolean DEFAULT false,
+ADD COLUMN IF NOT EXISTS is_on_campaign_wall boolean DEFAULT false;
+
+-- 2. Add indexes for better query performance
+CREATE INDEX IF NOT EXISTS idx_campaigns_is_open_for_applications ON public.campaigns(is_open_for_applications) WHERE is_open_for_applications = true;
+CREATE INDEX IF NOT EXISTS idx_campaigns_is_on_campaign_wall ON public.campaigns(is_on_campaign_wall) WHERE is_on_campaign_wall = true;
+CREATE INDEX IF NOT EXISTS idx_campaigns_open_and_wall ON public.campaigns(is_open_for_applications, is_on_campaign_wall) WHERE is_open_for_applications = true AND is_on_campaign_wall = true;
+
+-- 3. Add new columns to campaign_applications table
+ALTER TABLE public.campaign_applications
+ADD COLUMN IF NOT EXISTS payment_min numeric,
+ADD COLUMN IF NOT EXISTS payment_max numeric,
+ADD COLUMN IF NOT EXISTS timeline_days integer,
+ADD COLUMN IF NOT EXISTS timeline_weeks integer,
+ADD COLUMN IF NOT EXISTS description text;
+
+-- 4. Add index for campaign_applications status filtering
+CREATE INDEX IF NOT EXISTS idx_campaign_applications_status ON public.campaign_applications(status);
+CREATE INDEX IF NOT EXISTS idx_campaign_applications_campaign_status ON public.campaign_applications(campaign_id, status);
+CREATE INDEX IF NOT EXISTS idx_campaign_applications_creator_status ON public.campaign_applications(creator_id, status);
+
+-- 5. Add 'reviewing' to application_status enum if it doesn't exist
+DO $$
+BEGIN
+ -- Check if 'reviewing' value exists in the enum
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_enum
+ WHERE enumlabel = 'reviewing'
+ AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'application_status')
+ ) THEN
+ -- Add 'reviewing' to the enum
+ -- Note: PostgreSQL doesn't support IF NOT EXISTS for ALTER TYPE ADD VALUE
+ -- So we check first, then add if needed
+ ALTER TYPE application_status ADD VALUE 'reviewing';
+ END IF;
+EXCEPTION
+ WHEN duplicate_object THEN
+ -- Value already exists, ignore
+ NULL;
+END $$;
+
+-- 6. Add comment for documentation
+COMMENT ON COLUMN public.campaigns.is_open_for_applications IS 'Whether this campaign accepts applications from creators';
+COMMENT ON COLUMN public.campaigns.is_on_campaign_wall IS 'Whether this campaign is visible on the public campaign wall';
+COMMENT ON COLUMN public.campaign_applications.payment_min IS 'Minimum payment amount the creator is requesting';
+COMMENT ON COLUMN public.campaign_applications.payment_max IS 'Maximum payment amount the creator is requesting';
+COMMENT ON COLUMN public.campaign_applications.timeline_days IS 'Number of days the creator estimates to complete the campaign';
+COMMENT ON COLUMN public.campaign_applications.timeline_weeks IS 'Number of weeks the creator estimates to complete the campaign';
+COMMENT ON COLUMN public.campaign_applications.description IS 'Creator description explaining why they should be chosen for this campaign';
+
diff --git a/backend/app/__init__.py b/backend/app/__init__.py
new file mode 100644
index 0000000..2839885
--- /dev/null
+++ b/backend/app/__init__.py
@@ -0,0 +1 @@
+# app package init
diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py
new file mode 100644
index 0000000..04823ca
--- /dev/null
+++ b/backend/app/api/__init__.py
@@ -0,0 +1 @@
+# api package init
diff --git a/backend/app/api/routes/__init__.py b/backend/app/api/routes/__init__.py
new file mode 100644
index 0000000..8ebd595
--- /dev/null
+++ b/backend/app/api/routes/__init__.py
@@ -0,0 +1 @@
+# routes package init
diff --git a/backend/app/api/routes/ai_analytics.py b/backend/app/api/routes/ai_analytics.py
new file mode 100644
index 0000000..bf74ffa
--- /dev/null
+++ b/backend/app/api/routes/ai_analytics.py
@@ -0,0 +1,1242 @@
+"""
+AI-Powered Analytics endpoints for predictive analytics, insights, segmentation, etc.
+"""
+from fastapi import APIRouter, HTTPException, Depends, Query
+from pydantic import BaseModel
+from typing import Optional, List, Dict, Any
+from datetime import datetime, timezone, timedelta
+import json
+from groq import Groq
+from app.core.supabase_clients import supabase_anon
+from app.core.dependencies import get_current_user
+from app.core.config import settings
+
+router = APIRouter()
+
+
+def get_groq_client():
+ """Get Groq client instance"""
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+ return Groq(api_key=settings.groq_api_key)
+
+
+async def get_user_profile(user: dict):
+ """Get brand or creator profile based on user role"""
+ role = user.get("role")
+ user_id = user.get("id")
+
+ if role == "Brand":
+ brand_res = supabase_anon.table("brands") \
+ .select("*") \
+ .eq("user_id", user_id) \
+ .single() \
+ .execute()
+ if brand_res.data:
+ return {"type": "brand", "profile": brand_res.data}
+ elif role == "Creator":
+ creator_res = supabase_anon.table("creators") \
+ .select("*") \
+ .eq("user_id", user_id) \
+ .single() \
+ .execute()
+ if creator_res.data:
+ return {"type": "creator", "profile": creator_res.data}
+
+ raise HTTPException(
+ status_code=403,
+ detail="User profile not found. Please complete onboarding."
+ )
+
+
+def get_historical_metrics(brand_id: Optional[str] = None, creator_id: Optional[str] = None, campaign_id: Optional[str] = None):
+ """Fetch historical metrics data for analysis"""
+ # Build query for metric updates
+ query = supabase_anon.table("campaign_deliverable_metric_updates").select("*")
+
+ # Get metric IDs based on filters
+ metric_ids = []
+ has_filters = False
+
+ if brand_id:
+ has_filters = True
+ # Filter by brand through campaigns
+ campaigns_res = supabase_anon.table("campaigns") \
+ .select("id") \
+ .eq("brand_id", brand_id) \
+ .execute()
+ campaign_ids = [c["id"] for c in campaigns_res.data or []]
+ if campaign_ids:
+ deliverables_res = supabase_anon.table("campaign_deliverables") \
+ .select("id") \
+ .in_("campaign_id", campaign_ids) \
+ .execute()
+ deliverable_ids = [d["id"] for d in deliverables_res.data or []]
+ if deliverable_ids:
+ metrics_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("id") \
+ .in_("campaign_deliverable_id", deliverable_ids) \
+ .execute()
+ metric_ids = [m["id"] for m in metrics_res.data or []]
+
+ if campaign_id:
+ has_filters = True
+ deliverables_res = supabase_anon.table("campaign_deliverables") \
+ .select("id") \
+ .eq("campaign_id", campaign_id) \
+ .execute()
+ deliverable_ids = [d["id"] for d in deliverables_res.data or []]
+ if deliverable_ids:
+ metrics_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("id") \
+ .in_("campaign_deliverable_id", deliverable_ids) \
+ .execute()
+ campaign_metric_ids = [m["id"] for m in metrics_res.data or []]
+ if metric_ids:
+ # Intersect with brand filter
+ metric_ids = [m for m in metric_ids if m in campaign_metric_ids]
+ else:
+ metric_ids = campaign_metric_ids
+
+ # Apply metric filters only if we have specific filters
+ # If no filters, get all updates (for creator-only case)
+ if has_filters:
+ if metric_ids:
+ query = query.in_("campaign_deliverable_metric_id", metric_ids)
+ else:
+ # If we have filters but no metrics found, return empty
+ # This means the brand/campaign exists but has no metrics/updates yet
+ return []
+
+ if creator_id:
+ query = query.eq("submitted_by", creator_id)
+
+ # Execute query
+ try:
+ result = query.order("submitted_at", desc=False).limit(1000).execute()
+ updates = result.data or []
+ except Exception as e:
+ # If query fails, return empty
+ return []
+
+ if not updates:
+ return []
+
+ # Enrich with metric and deliverable data
+ metric_ids_from_updates = list(set([u["campaign_deliverable_metric_id"] for u in updates if u.get("campaign_deliverable_metric_id")]))
+
+ if metric_ids_from_updates:
+ metrics_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("id, name, display_name, campaign_deliverable_id") \
+ .in_("id", metric_ids_from_updates) \
+ .execute()
+ metrics = {m["id"]: m for m in (metrics_res.data or [])}
+ else:
+ metrics = {}
+
+ deliverable_ids = list(set([m.get("campaign_deliverable_id") for m in metrics.values() if m.get("campaign_deliverable_id")]))
+ if deliverable_ids:
+ deliverables_res = supabase_anon.table("campaign_deliverables") \
+ .select("id, campaign_id, platform, content_type") \
+ .in_("id", deliverable_ids) \
+ .execute()
+ deliverables = {d["id"]: d for d in (deliverables_res.data or [])}
+ else:
+ deliverables = {}
+
+ # Combine data
+ enriched_updates = []
+ for update in updates:
+ metric_id = update.get("campaign_deliverable_metric_id")
+ metric = metrics.get(metric_id, {}) if metric_id else {}
+ deliverable_id = metric.get("campaign_deliverable_id")
+ deliverable = deliverables.get(deliverable_id, {}) if deliverable_id else {}
+
+ enriched_update = {
+ **update,
+ "campaign_deliverable_metrics": metric,
+ "campaign_deliverables": deliverable
+ }
+ enriched_updates.append(enriched_update)
+
+ return enriched_updates
+
+
+# ==================== Pydantic Models ====================
+
+class PredictiveAnalyticsRequest(BaseModel):
+ campaign_id: Optional[str] = None
+ metric_type: Optional[str] = None # 'performance', 'roi', 'engagement'
+ forecast_periods: int = 30 # days
+
+
+class PredictiveAnalyticsResponse(BaseModel):
+ forecast: Dict[str, Any]
+ confidence: str
+ factors: List[str]
+ recommendations: List[str]
+
+
+class AutomatedInsightsResponse(BaseModel):
+ summary: str
+ trends: List[str]
+ anomalies: List[Dict[str, Any]]
+ recommendations: List[str]
+ key_metrics: Dict[str, Any]
+
+
+class AudienceSegmentationResponse(BaseModel):
+ segments: List[Dict[str, Any]]
+ visualization_data: Dict[str, Any]
+
+
+class SentimentAnalysisRequest(BaseModel):
+ text: Optional[str] = None
+ campaign_id: Optional[str] = None
+
+
+class SentimentAnalysisResponse(BaseModel):
+ overall_sentiment: str
+ sentiment_score: float
+ positive_aspects: List[str]
+ negative_aspects: List[str]
+ recommendations: List[str]
+
+
+class AnomalyDetectionResponse(BaseModel):
+ anomalies: List[Dict[str, Any]]
+ summary: str
+
+
+class AttributionModelingResponse(BaseModel):
+ attribution: Dict[str, float]
+ top_contributors: List[Dict[str, Any]]
+ insights: List[str]
+
+
+class BenchmarkingResponse(BaseModel):
+ your_metrics: Dict[str, float]
+ industry_benchmarks: Dict[str, float]
+ comparison: Dict[str, Any]
+ recommendations: List[str]
+
+
+class ChurnPredictionResponse(BaseModel):
+ churn_risk: Dict[str, float]
+ at_risk_segments: List[Dict[str, Any]]
+ recommendations: List[str]
+
+
+class NaturalLanguageQueryRequest(BaseModel):
+ query: str
+ campaign_id: Optional[str] = None
+
+
+class NaturalLanguageQueryResponse(BaseModel):
+ answer: str
+ data_sources: List[str]
+ confidence: str
+
+
+class KPIOptimizationResponse(BaseModel):
+ current_kpis: Dict[str, float]
+ optimization_suggestions: List[Dict[str, Any]]
+ priority_actions: List[str]
+
+
+# ==================== API Endpoints ====================
+
+@router.post("/analytics/ai/predictive", response_model=PredictiveAnalyticsResponse)
+async def get_predictive_analytics(
+ request: PredictiveAnalyticsRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Forecast campaign performance, ROI, or audience engagement using historical data"""
+ try:
+ user_profile = await get_user_profile(user)
+ profile = user_profile["profile"]
+ user_type = user_profile["type"]
+
+ historical_data = get_historical_metrics(
+ brand_id=profile["id"] if user_type == "brand" else None,
+ creator_id=profile["id"] if user_type == "creator" else None,
+ campaign_id=request.campaign_id
+ )
+
+ if not historical_data:
+ return PredictiveAnalyticsResponse(
+ forecast={
+ "predicted_value": 0.0,
+ "trend": "stable",
+ "growth_rate": 0.0,
+ "forecasted_values": []
+ },
+ confidence="low",
+ factors=["No historical data available"],
+ recommendations=[
+ "Start by creating metrics for your campaign deliverables",
+ "Have creators submit metric values to build historical data",
+ "Once you have at least 5-10 data points, predictions will be available"
+ ]
+ )
+
+ # Prepare data for AI analysis
+ metrics_summary = {}
+ for entry in historical_data[-30:]: # Last 30 entries
+ metric_name = entry.get("campaign_deliverable_metrics", {}).get("name", "unknown")
+ value = entry.get("value", 0)
+ date = entry.get("submitted_at", "")
+ if metric_name not in metrics_summary:
+ metrics_summary[metric_name] = []
+ metrics_summary[metric_name].append({"value": value, "date": date})
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Analyze this historical campaign metrics data and provide predictive analytics:
+
+HISTORICAL DATA:
+{json.dumps(metrics_summary, indent=2)}
+
+METRIC TYPE: {request.metric_type or 'general performance'}
+FORECAST PERIOD: {request.forecast_periods} days
+USER TYPE: {user_type}
+
+Based on the historical trends, provide:
+1. Forecasted values for the next {request.forecast_periods} days
+2. Confidence level (high/medium/low)
+3. Key factors influencing the forecast
+4. Actionable recommendations
+
+Return your response as JSON with this exact structure:
+{{
+ "forecast": {{
+ "predicted_value": 0.0,
+ "trend": "increasing|decreasing|stable",
+ "growth_rate": 0.0,
+ "forecasted_values": [{{"date": "YYYY-MM-DD", "value": 0.0}}]
+ }},
+ "confidence": "high|medium|low",
+ "factors": ["Factor 1", "Factor 2"],
+ "recommendations": ["Recommendation 1", "Recommendation 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert data analyst specializing in predictive analytics for marketing campaigns. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.3,
+ max_completion_tokens=1500,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return PredictiveAnalyticsResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error generating predictive analytics: {str(e)}")
+
+
+@router.get("/analytics/ai/insights", response_model=AutomatedInsightsResponse)
+async def get_automated_insights(
+ campaign_id: Optional[str] = Query(None),
+ user: dict = Depends(get_current_user)
+):
+ """Generate plain-language summaries of analytics data with trends, anomalies, and recommendations"""
+ try:
+ user_profile = await get_user_profile(user)
+ profile = user_profile["profile"]
+ user_type = user_profile["type"]
+
+ historical_data = get_historical_metrics(
+ brand_id=profile["id"] if user_type == "brand" else None,
+ creator_id=profile["id"] if user_type == "creator" else None,
+ campaign_id=campaign_id
+ )
+
+ if not historical_data:
+ return AutomatedInsightsResponse(
+ summary="No analytics data available yet. Start tracking metrics to get automated insights.",
+ trends=[],
+ anomalies=[],
+ recommendations=[
+ "Create metrics for your campaign deliverables",
+ "Have creators submit metric values",
+ "Once you have data, insights will appear here automatically"
+ ],
+ key_metrics={}
+ )
+
+ # Aggregate metrics
+ metrics_by_name = {}
+ for entry in historical_data:
+ metric_name = entry.get("campaign_deliverable_metrics", {}).get("display_name") or entry.get("campaign_deliverable_metrics", {}).get("name", "unknown")
+ value = float(entry.get("value", 0))
+ date = entry.get("submitted_at", "")
+
+ if metric_name not in metrics_by_name:
+ metrics_by_name[metric_name] = []
+ metrics_by_name[metric_name].append({"value": value, "date": date})
+
+ # Calculate trends
+ trends_data = {}
+ for metric_name, values in metrics_by_name.items():
+ if len(values) >= 2:
+ recent_avg = sum(v["value"] for v in values[-7:]) / min(7, len(values))
+ older_avg = sum(v["value"] for v in values[:-7]) / max(1, len(values) - 7) if len(values) > 7 else recent_avg
+ change = ((recent_avg - older_avg) / older_avg * 100) if older_avg > 0 else 0
+ trends_data[metric_name] = {
+ "current_avg": recent_avg,
+ "previous_avg": older_avg,
+ "change_percent": change,
+ "trend": "increasing" if change > 5 else "decreasing" if change < -5 else "stable"
+ }
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Analyze this campaign analytics data and provide automated insights:
+
+METRICS DATA:
+{json.dumps(metrics_by_name, indent=2)}
+
+TRENDS ANALYSIS:
+{json.dumps(trends_data, indent=2)}
+
+USER TYPE: {user_type}
+
+Provide:
+1. A plain-language executive summary (2-3 sentences)
+2. Key trends identified
+3. Any anomalies or unusual patterns
+4. Actionable recommendations
+
+Return your response as JSON with this exact structure:
+{{
+ "summary": "Executive summary in plain language",
+ "trends": ["Trend 1", "Trend 2"],
+ "anomalies": [{{"metric": "Metric name", "description": "Anomaly description", "severity": "high|medium|low"}}],
+ "recommendations": ["Recommendation 1", "Recommendation 2"],
+ "key_metrics": {{"metric_name": "value"}}
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert analytics consultant. Provide clear, actionable insights in plain language. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.4,
+ max_completion_tokens=1200,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return AutomatedInsightsResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error generating insights: {str(e)}")
+
+
+@router.get("/analytics/ai/audience-segmentation", response_model=AudienceSegmentationResponse)
+async def get_audience_segmentation(
+ campaign_id: Optional[str] = Query(None),
+ user: dict = Depends(get_current_user)
+):
+ """Use AI to identify and visualize key audience segments based on demographics, interests, and behaviors"""
+ try:
+ user_profile = await get_user_profile(user)
+ profile = user_profile["profile"]
+
+ historical_data = get_historical_metrics(
+ brand_id=profile["id"] if user_profile["type"] == "brand" else None,
+ creator_id=profile["id"] if user_profile["type"] == "creator" else None,
+ campaign_id=campaign_id
+ )
+
+ # Extract demographics data
+ demographics_data = []
+ for entry in historical_data:
+ demographics = entry.get("demographics")
+ if demographics and isinstance(demographics, dict):
+ demographics_data.append(demographics)
+
+ if not demographics_data:
+ # Return default segments if no demographics data
+ return AudienceSegmentationResponse(
+ segments=[
+ {"name": "General Audience", "size": 100, "characteristics": ["No demographic data available"]}
+ ],
+ visualization_data={}
+ )
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Analyze this audience demographics data and identify key segments:
+
+DEMOGRAPHICS DATA:
+{json.dumps(demographics_data[:50], indent=2)} # Limit to 50 for prompt size
+
+Identify distinct audience segments based on:
+- Demographics (age, gender, location)
+- Interests
+- Behaviors
+- Engagement patterns
+
+Return your response as JSON with this exact structure:
+{{
+ "segments": [
+ {{
+ "name": "Segment Name",
+ "size": 25,
+ "characteristics": ["Characteristic 1", "Characteristic 2"],
+ "demographics": {{"age_range": "25-34", "gender": "mixed", "location": "urban"}},
+ "interests": ["Interest 1", "Interest 2"],
+ "engagement_score": 0.75
+ }}
+ ],
+ "visualization_data": {{
+ "segment_sizes": {{"Segment 1": 25, "Segment 2": 30}},
+ "demographic_breakdown": {{}}
+ }}
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert in audience segmentation and market research. Identify meaningful audience segments. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.5,
+ max_completion_tokens=1500,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return AudienceSegmentationResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error generating audience segmentation: {str(e)}")
+
+
+@router.post("/analytics/ai/sentiment", response_model=SentimentAnalysisResponse)
+async def analyze_sentiment(
+ request: SentimentAnalysisRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Analyze social media and campaign feedback to gauge public sentiment"""
+ try:
+ # Get feedback data if campaign_id provided
+ feedback_texts = []
+ if request.campaign_id:
+ feedback_res = supabase_anon.table("campaign_deliverable_metric_feedback") \
+ .select("feedback_text") \
+ .execute()
+ feedback_texts = [f["feedback_text"] for f in feedback_res.data or [] if f.get("feedback_text")]
+
+ if request.text:
+ feedback_texts.append(request.text)
+
+ if not feedback_texts:
+ raise HTTPException(status_code=400, detail="No text data provided for sentiment analysis")
+
+ combined_text = "\n\n".join(feedback_texts[:20]) # Limit to 20 feedback items
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Analyze the sentiment of this campaign feedback and social media data:
+
+FEEDBACK DATA:
+{combined_text}
+
+Provide a comprehensive sentiment analysis including:
+1. Overall sentiment (positive, neutral, negative, mixed)
+2. Sentiment score from -1 (very negative) to 1 (very positive)
+3. Positive aspects mentioned
+4. Negative aspects mentioned
+5. Recommendations for improvement
+
+Return your response as JSON with this exact structure:
+{{
+ "overall_sentiment": "positive|neutral|negative|mixed",
+ "sentiment_score": 0.75,
+ "positive_aspects": ["Aspect 1", "Aspect 2"],
+ "negative_aspects": ["Aspect 1", "Aspect 2"],
+ "recommendations": ["Recommendation 1", "Recommendation 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert sentiment analyst specializing in brand and campaign feedback. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.3,
+ max_completion_tokens=1000,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return SentimentAnalysisResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error analyzing sentiment: {str(e)}")
+
+
+@router.get("/analytics/ai/anomaly-detection", response_model=AnomalyDetectionResponse)
+async def detect_anomalies(
+ campaign_id: Optional[str] = Query(None),
+ user: dict = Depends(get_current_user)
+):
+ """Automatically flag unusual spikes or drops in metrics"""
+ try:
+ user_profile = await get_user_profile(user)
+ profile = user_profile["profile"]
+
+ historical_data = get_historical_metrics(
+ brand_id=profile["id"] if user_profile["type"] == "brand" else None,
+ creator_id=profile["id"] if user_profile["type"] == "creator" else None,
+ campaign_id=campaign_id
+ )
+
+ if len(historical_data) < 5:
+ return AnomalyDetectionResponse(
+ anomalies=[],
+ summary="Insufficient data for anomaly detection (need at least 5 data points)"
+ )
+
+ # Organize by metric
+ metrics_data = {}
+ for entry in historical_data:
+ metric_name = entry.get("campaign_deliverable_metrics", {}).get("display_name") or entry.get("campaign_deliverable_metrics", {}).get("name", "unknown")
+ value = float(entry.get("value", 0))
+ date = entry.get("submitted_at", "")
+
+ if metric_name not in metrics_data:
+ metrics_data[metric_name] = []
+ metrics_data[metric_name].append({"value": value, "date": date})
+
+ # Calculate basic statistics for anomaly detection
+ stats = {}
+ for metric_name, values in metrics_data.items():
+ if len(values) >= 3:
+ vals = [v["value"] for v in values]
+ mean = sum(vals) / len(vals)
+ variance = sum((x - mean) ** 2 for x in vals) / len(vals)
+ std_dev = variance ** 0.5
+ stats[metric_name] = {
+ "mean": mean,
+ "std_dev": std_dev,
+ "values": values[-10:] # Last 10 values
+ }
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Analyze this metrics data and detect anomalies (unusual spikes or drops):
+
+METRICS DATA WITH STATISTICS:
+{json.dumps(stats, indent=2)}
+
+Identify anomalies where:
+- Values are significantly above or below the mean (more than 2 standard deviations)
+- Sudden spikes or drops in trends
+- Unusual patterns
+
+Return your response as JSON with this exact structure:
+{{
+ "anomalies": [
+ {{
+ "metric": "Metric name",
+ "date": "YYYY-MM-DD",
+ "value": 0.0,
+ "expected_value": 0.0,
+ "deviation": 0.0,
+ "severity": "high|medium|low",
+ "description": "Description of the anomaly"
+ }}
+ ],
+ "summary": "Summary of detected anomalies"
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert data analyst specializing in anomaly detection. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.2,
+ max_completion_tokens=1200,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return AnomalyDetectionResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error detecting anomalies: {str(e)}")
+
+
+@router.get("/analytics/ai/attribution", response_model=AttributionModelingResponse)
+async def get_attribution_modeling(
+ campaign_id: Optional[str] = Query(None),
+ user: dict = Depends(get_current_user)
+):
+ """Use AI to determine which channels, creators, or content types contribute most to conversions"""
+ try:
+ user_profile = await get_user_profile(user)
+ profile = user_profile["profile"]
+
+ historical_data = get_historical_metrics(
+ brand_id=profile["id"] if user_profile["type"] == "brand" else None,
+ creator_id=profile["id"] if user_profile["type"] == "creator" else None,
+ campaign_id=campaign_id
+ )
+
+ # Organize by platform/channel/creator
+ attribution_data = {}
+ for entry in historical_data:
+ metric_data = entry.get("campaign_deliverable_metrics", {})
+ deliverable_data = metric_data.get("campaign_deliverables", {}) if isinstance(metric_data.get("campaign_deliverables"), dict) else {}
+ platform = deliverable_data.get("platform", "unknown")
+ content_type = deliverable_data.get("content_type", "unknown")
+ value = float(entry.get("value", 0))
+
+ key = f"{platform}_{content_type}"
+ if key not in attribution_data:
+ attribution_data[key] = {
+ "platform": platform,
+ "content_type": content_type,
+ "total_value": 0,
+ "count": 0,
+ "avg_value": 0
+ }
+ attribution_data[key]["total_value"] += value
+ attribution_data[key]["count"] += 1
+
+ # Calculate averages
+ for key, data in attribution_data.items():
+ data["avg_value"] = data["total_value"] / data["count"] if data["count"] > 0 else 0
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Analyze this attribution data and determine which channels, creators, or content types contribute most:
+
+ATTRIBUTION DATA:
+{json.dumps(attribution_data, indent=2)}
+
+Determine:
+1. Attribution percentages for each channel/content type
+2. Top contributors to conversions/engagement
+3. Insights about what's working best
+
+Return your response as JSON with this exact structure:
+{{
+ "attribution": {{
+ "Channel/Content Type": 25.5
+ }},
+ "top_contributors": [
+ {{
+ "name": "Channel/Content Type",
+ "contribution_percent": 25.5,
+ "total_value": 1000,
+ "insight": "Why this is effective"
+ }}
+ ],
+ "insights": ["Insight 1", "Insight 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert in marketing attribution modeling. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.3,
+ max_completion_tokens=1000,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return AttributionModelingResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error generating attribution model: {str(e)}")
+
+
+@router.get("/analytics/ai/benchmarking", response_model=BenchmarkingResponse)
+async def get_benchmarking(
+ campaign_id: Optional[str] = Query(None),
+ user: dict = Depends(get_current_user)
+):
+ """Compare brand's performance against industry standards using AI-driven benchmarks"""
+ try:
+ user_profile = await get_user_profile(user)
+ profile = user_profile["profile"]
+
+ historical_data = get_historical_metrics(
+ brand_id=profile["id"] if user_profile["type"] == "brand" else None,
+ creator_id=profile["id"] if user_profile["type"] == "creator" else None,
+ campaign_id=campaign_id
+ )
+
+ if not historical_data:
+ return BenchmarkingResponse(
+ your_metrics={},
+ industry_benchmarks={},
+ comparison={},
+ recommendations=[
+ "No metric data available yet. Start tracking metrics to compare against industry benchmarks.",
+ "Create metrics for your deliverables and have creators submit values",
+ "Once you have data, benchmarking will be available"
+ ]
+ )
+
+ # Calculate your metrics
+ your_metrics = {}
+ total_value = 0
+ count = 0
+ for entry in historical_data:
+ value = float(entry.get("value", 0))
+ total_value += value
+ count += 1
+
+ if count > 0:
+ your_metrics = {
+ "avg_engagement": total_value / count,
+ "total_engagement": total_value,
+ "data_points": count
+ }
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Compare these campaign metrics against industry benchmarks:
+
+YOUR METRICS:
+{json.dumps(your_metrics, indent=2)}
+
+Provide:
+1. Industry benchmark values for similar campaigns
+2. Comparison showing how you perform vs industry
+3. Recommendations for improvement
+
+Return your response as JSON with this exact structure:
+{{
+ "your_metrics": {{
+ "metric_name": 0.0
+ }},
+ "industry_benchmarks": {{
+ "metric_name": 0.0
+ }},
+ "comparison": {{
+ "metric_name": {{
+ "your_value": 0.0,
+ "industry_avg": 0.0,
+ "percentile": 75,
+ "status": "above|below|at average"
+ }}
+ }},
+ "recommendations": ["Recommendation 1", "Recommendation 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert in marketing analytics and industry benchmarking. Provide realistic industry benchmarks. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.4,
+ max_completion_tokens=1200,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return BenchmarkingResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error generating benchmarks: {str(e)}")
+
+
+@router.get("/analytics/ai/churn-prediction", response_model=ChurnPredictionResponse)
+async def predict_churn(
+ campaign_id: Optional[str] = Query(None),
+ user: dict = Depends(get_current_user)
+):
+ """Predict which audience segments or customers are likely to disengage or churn"""
+ try:
+ user_profile = await get_user_profile(user)
+ profile = user_profile["profile"]
+
+ historical_data = get_historical_metrics(
+ brand_id=profile["id"] if user_profile["type"] == "brand" else None,
+ creator_id=profile["id"] if user_profile["type"] == "creator" else None,
+ campaign_id=campaign_id
+ )
+
+ if len(historical_data) < 10:
+ return ChurnPredictionResponse(
+ churn_risk={},
+ at_risk_segments=[],
+ recommendations=["Insufficient data for churn prediction. Need at least 10 data points."]
+ )
+
+ # Analyze engagement trends
+ engagement_trends = {}
+ for entry in historical_data[-30:]: # Last 30 entries
+ date = entry.get("submitted_at", "")
+ value = float(entry.get("value", 0))
+ if date:
+ engagement_trends[date] = value
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Analyze this engagement data and predict churn risk:
+
+ENGAGEMENT TRENDS:
+{json.dumps(engagement_trends, indent=2)}
+
+Identify:
+1. Churn risk levels for different segments
+2. At-risk audience segments
+3. Recommendations to prevent churn
+
+Return your response as JSON with this exact structure:
+{{
+ "churn_risk": {{
+ "segment_name": 0.75
+ }},
+ "at_risk_segments": [
+ {{
+ "segment": "Segment name",
+ "risk_score": 0.75,
+ "indicators": ["Indicator 1", "Indicator 2"],
+ "recommendations": ["Recommendation 1"]
+ }}
+ ],
+ "recommendations": ["General recommendation 1", "General recommendation 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert in customer retention and churn prediction. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.3,
+ max_completion_tokens=1000,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return ChurnPredictionResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error predicting churn: {str(e)}")
+
+
+@router.post("/analytics/ai/natural-language-query", response_model=NaturalLanguageQueryResponse)
+async def natural_language_query(
+ request: NaturalLanguageQueryRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Let users ask questions about their analytics data in plain English and get AI-generated answers"""
+ try:
+ user_profile = await get_user_profile(user)
+ profile = user_profile["profile"]
+
+ historical_data = get_historical_metrics(
+ brand_id=profile["id"] if user_profile["type"] == "brand" else None,
+ creator_id=profile["id"] if user_profile["type"] == "creator" else None,
+ campaign_id=request.campaign_id
+ )
+
+ # Prepare summary of available data
+ data_summary = {
+ "total_data_points": len(historical_data),
+ "metrics": list(set([
+ entry.get("campaign_deliverable_metrics", {}).get("display_name") or
+ entry.get("campaign_deliverable_metrics", {}).get("name", "unknown")
+ for entry in historical_data
+ ])),
+ "date_range": {
+ "earliest": historical_data[0].get("submitted_at") if historical_data else None,
+ "latest": historical_data[-1].get("submitted_at") if historical_data else None
+ },
+ "recent_values": [
+ {
+ "metric": entry.get("campaign_deliverable_metrics", {}).get("display_name") or "unknown",
+ "value": entry.get("value"),
+ "date": entry.get("submitted_at")
+ }
+ for entry in historical_data[-10:]
+ ]
+ }
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Answer this question about campaign analytics data:
+
+USER QUESTION: {request.query}
+
+AVAILABLE DATA SUMMARY:
+{json.dumps(data_summary, indent=2)}
+
+Provide a clear, helpful answer based on the available data. If the question cannot be answered with the available data, say so.
+
+Return your response as JSON with this exact structure:
+{{
+ "answer": "Clear answer to the user's question",
+ "data_sources": ["Data source 1", "Data source 2"],
+ "confidence": "high|medium|low"
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are a helpful analytics assistant. Answer questions about campaign data clearly and accurately. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.5,
+ max_completion_tokens=800,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return NaturalLanguageQueryResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error processing query: {str(e)}")
+
+
+@router.get("/analytics/ai/kpi-optimization", response_model=KPIOptimizationResponse)
+async def get_kpi_optimization(
+ campaign_id: Optional[str] = Query(None),
+ user: dict = Depends(get_current_user)
+):
+ """Recommend actions to improve key metrics based on AI analysis"""
+ try:
+ user_profile = await get_user_profile(user)
+ profile = user_profile["profile"]
+
+ historical_data = get_historical_metrics(
+ brand_id=profile["id"] if user_profile["type"] == "brand" else None,
+ creator_id=profile["id"] if user_profile["type"] == "creator" else None,
+ campaign_id=campaign_id
+ )
+
+ if not historical_data:
+ return KPIOptimizationResponse(
+ current_kpis={},
+ optimization_suggestions=[],
+ priority_actions=[
+ "No metric data available yet. Start by creating metrics for your deliverables.",
+ "Have creators submit metric values to enable KPI optimization suggestions.",
+ "Once you have data, AI-powered optimization recommendations will appear here."
+ ]
+ )
+
+ # Calculate current KPIs
+ current_kpis = {}
+ metrics_summary = {}
+ for entry in historical_data[-30:]: # Last 30 entries
+ metric_name = entry.get("campaign_deliverable_metrics", {}).get("display_name") or entry.get("campaign_deliverable_metrics", {}).get("name", "unknown")
+ value = float(entry.get("value", 0))
+
+ if metric_name not in metrics_summary:
+ metrics_summary[metric_name] = []
+ metrics_summary[metric_name].append(value)
+
+ for metric_name, values in metrics_summary.items():
+ current_kpis[metric_name] = {
+ "current_avg": sum(values) / len(values) if values else 0,
+ "trend": "increasing" if len(values) >= 2 and values[-1] > values[0] else "decreasing" if len(values) >= 2 and values[-1] < values[0] else "stable"
+ }
+
+ groq_client = get_groq_client()
+
+ prompt = f"""Analyze these KPIs and provide optimization recommendations:
+
+CURRENT KPIs:
+{json.dumps(current_kpis, indent=2)}
+
+METRICS SUMMARY:
+{json.dumps({k: {"values": v, "count": len(v)} for k, v in metrics_summary.items()}, indent=2)}
+
+Provide:
+1. Optimization suggestions for each KPI
+2. Priority actions to improve metrics
+3. Specific, actionable recommendations
+
+Return your response as JSON with this exact structure:
+{{
+ "current_kpis": {{
+ "KPI name": 0.0
+ }},
+ "optimization_suggestions": [
+ {{
+ "kpi": "KPI name",
+ "current_value": 0.0,
+ "target_value": 0.0,
+ "suggestions": ["Suggestion 1", "Suggestion 2"],
+ "expected_impact": "high|medium|low"
+ }}
+ ],
+ "priority_actions": ["Action 1", "Action 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert in KPI optimization and performance improvement. Provide actionable, specific recommendations. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.4,
+ max_completion_tokens=1500,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+ return KPIOptimizationResponse(**result)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error generating KPI optimization: {str(e)}")
+
diff --git a/backend/app/api/routes/analytics.py b/backend/app/api/routes/analytics.py
new file mode 100644
index 0000000..90a7b2e
--- /dev/null
+++ b/backend/app/api/routes/analytics.py
@@ -0,0 +1,1805 @@
+"""
+Campaign Performance Analytics & ROI Tracking routes.
+Supports brands defining metrics, creators submitting data, AI screenshot extraction, and feedback.
+"""
+from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Query
+from pydantic import BaseModel, Field
+from typing import Optional, List, Dict, Any
+from datetime import datetime, timezone, timedelta
+from uuid import UUID
+import base64
+import httpx
+import json
+from groq import Groq
+from app.core.supabase_clients import supabase_anon
+from app.core.dependencies import get_current_brand, get_current_creator, get_current_user
+from app.core.config import settings
+
+router = APIRouter()
+
+# Gemini Vision API for screenshot extraction
+GEMINI_API_KEY = settings.gemini_api_key
+GEMINI_VISION_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent"
+
+
+# ==================== Pydantic Models ====================
+
+class MetricCreate(BaseModel):
+ """Schema for creating a metric for a deliverable"""
+ campaign_deliverable_id: str
+ name: str
+ display_name: Optional[str] = None
+ target_value: Optional[float] = None
+ is_custom: bool = False
+
+
+class MetricUpdate(BaseModel):
+ """Schema for updating a metric"""
+ display_name: Optional[str] = None
+ target_value: Optional[float] = None
+
+
+class MetricResponse(BaseModel):
+ """Schema for metric response"""
+ id: str
+ campaign_deliverable_id: str
+ name: str
+ display_name: Optional[str]
+ target_value: Optional[float]
+ is_custom: bool
+ created_at: datetime
+
+
+class MetricValueSubmit(BaseModel):
+ """Schema for submitting a metric value"""
+ value: float
+ demographics: Optional[Dict[str, Any]] = None
+ screenshot_url: Optional[str] = None
+ ai_extracted_data: Optional[Dict[str, Any]] = None
+
+
+class MetricUpdateResponse(BaseModel):
+ """Schema for metric update response"""
+ id: str
+ campaign_deliverable_metric_id: str
+ value: float
+ demographics: Optional[Dict[str, Any]]
+ screenshot_url: Optional[str]
+ ai_extracted_data: Optional[Dict[str, Any]]
+ submitted_by: str
+ submitted_at: datetime
+
+
+class FeedbackCreate(BaseModel):
+ """Schema for creating feedback on a metric update"""
+ feedback_text: str
+
+
+class CreatorCommentCreate(BaseModel):
+ """Schema for creator comments on metrics"""
+ comment_text: str
+ metric_update_id: Optional[str] = None
+
+
+class FeedbackResponse(BaseModel):
+ """Schema for feedback response"""
+ id: str
+ metric_update_id: str
+ brand_id: str
+ feedback_text: str
+ created_at: datetime
+
+
+class UpdateRequestCreate(BaseModel):
+ """Schema for requesting a metric update"""
+ campaign_deliverable_metric_id: Optional[str] = None # None = request all metrics
+ creator_id: str
+
+
+class UpdateRequestResponse(BaseModel):
+ """Schema for update request response"""
+ id: str
+ campaign_deliverable_metric_id: Optional[str]
+ brand_id: str
+ creator_id: str
+ requested_at: datetime
+ status: str
+
+
+class AnalyticsDashboardResponse(BaseModel):
+ """Schema for analytics dashboard data"""
+ campaign_id: str
+ campaign_title: str
+ platforms: List[Dict[str, Any]]
+ total_deliverables: int
+ total_metrics: int
+ metrics_with_updates: int
+ overall_progress: float
+
+
+# ==================== Helper Functions ====================
+
+async def extract_metrics_from_image(image_base64: str) -> Dict[str, Any]:
+ """
+ Use Gemini Vision API to extract metrics from a screenshot.
+ Returns structured data with extracted metric values.
+ """
+ if not GEMINI_API_KEY:
+ raise HTTPException(
+ status_code=500,
+ detail="Gemini API is not configured. Please set GEMINI_API_KEY in environment."
+ )
+
+ prompt = """
+ Analyze this social media analytics screenshot and extract all visible metrics.
+ Look for numbers related to:
+ - Impressions/Reach
+ - Views
+ - Likes/Reactions
+ - Comments
+ - Shares/Reposts
+ - Saves
+ - Engagement Rate
+ - Click-through Rate (CTR)
+ - Conversions
+ - Any other performance metrics visible
+
+ Return a JSON object with the metric names as keys and their numeric values.
+ Only include metrics that are clearly visible in the screenshot.
+ Format: {"impressions": 12345, "likes": 567, "comments": 89, ...}
+ """
+
+ payload = {
+ "contents": [{
+ "role": "user",
+ "parts": [
+ {"text": prompt},
+ {
+ "inline_data": {
+ "mime_type": "image/jpeg",
+ "data": image_base64
+ }
+ }
+ ]
+ }]
+ }
+
+ headers = {"Content-Type": "application/json"}
+ params = {"key": GEMINI_API_KEY}
+
+ try:
+ async with httpx.AsyncClient(timeout=30.0) as client:
+ response = await client.post(
+ GEMINI_VISION_API_URL,
+ json=payload,
+ headers=headers,
+ params=params
+ )
+ response.raise_for_status()
+ result = response.json()
+
+ # Extract text from Gemini response
+ if result.get("candidates") and result["candidates"][0].get("content"):
+ text = result["candidates"][0]["content"]["parts"][0].get("text", "")
+ # Try to parse JSON from the response
+ import json
+ import re
+ # Extract JSON from markdown code blocks if present
+ json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', text, re.DOTALL)
+ if json_match:
+ text = json_match.group(1)
+ else:
+ # Try to find JSON object in the text
+ json_match = re.search(r'\{.*\}', text, re.DOTALL)
+ if json_match:
+ text = json_match.group(0)
+
+ try:
+ extracted_data = json.loads(text)
+ return extracted_data
+ except json.JSONDecodeError:
+ # If JSON parsing fails, return the raw text
+ return {"raw_text": text, "parsed": False}
+ return {"error": "No data extracted"}
+ except httpx.RequestError as e:
+ raise HTTPException(status_code=502, detail=f"Gemini API error: {str(e)}")
+ except httpx.HTTPStatusError as e:
+ raise HTTPException(status_code=502, detail=f"Gemini API error: {str(e)}")
+
+
+# ==================== Brand Endpoints ====================
+
+@router.post("/analytics/metrics", response_model=MetricResponse, status_code=201)
+async def create_metric(
+ metric: MetricCreate,
+ brand: dict = Depends(get_current_brand)
+):
+ """Create a metric for a campaign deliverable"""
+ try:
+ # Verify deliverable belongs to brand's campaign
+ deliverable_res = supabase_anon.table("campaign_deliverables") \
+ .select("campaign_id") \
+ .eq("id", metric.campaign_deliverable_id) \
+ .execute()
+
+ if not deliverable_res.data:
+ raise HTTPException(status_code=404, detail="Deliverable not found")
+
+ campaign_id = deliverable_res.data[0]["campaign_id"]
+ campaign_res = supabase_anon.table("campaigns") \
+ .select("brand_id") \
+ .eq("id", campaign_id) \
+ .execute()
+
+ if not campaign_res.data or campaign_res.data[0]["brand_id"] != brand["id"]:
+ raise HTTPException(status_code=403, detail="Not authorized to create metrics for this deliverable")
+
+ # Create metric
+ metric_data = {
+ "campaign_deliverable_id": metric.campaign_deliverable_id,
+ "name": metric.name,
+ "display_name": metric.display_name or metric.name,
+ "target_value": metric.target_value,
+ "is_custom": metric.is_custom
+ }
+
+ result = supabase_anon.table("campaign_deliverable_metrics") \
+ .insert(metric_data) \
+ .execute()
+
+ if not result.data:
+ raise HTTPException(status_code=500, detail="Failed to create metric")
+
+ return MetricResponse(**result.data[0])
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error creating metric: {str(e)}")
+
+
+@router.get("/analytics/metrics/{metric_id}", response_model=MetricResponse)
+async def get_metric(
+ metric_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """Get a metric by ID"""
+ try:
+ result = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("*, campaign_deliverables(campaign_id)") \
+ .eq("id", metric_id) \
+ .execute()
+
+ if not result.data:
+ raise HTTPException(status_code=404, detail="Metric not found")
+
+ metric = result.data[0]
+ campaign_id = metric["campaign_deliverables"]["campaign_id"]
+ campaign_res = supabase_anon.table("campaigns") \
+ .select("brand_id") \
+ .eq("id", campaign_id) \
+ .execute()
+
+ if not campaign_res.data or campaign_res.data[0]["brand_id"] != brand["id"]:
+ raise HTTPException(status_code=403, detail="Not authorized")
+
+ return MetricResponse(**metric)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching metric: {str(e)}")
+
+
+@router.put("/analytics/metrics/{metric_id}", response_model=MetricResponse)
+async def update_metric(
+ metric_id: str,
+ metric_update: MetricUpdate,
+ brand: dict = Depends(get_current_brand)
+):
+ """Update a metric"""
+ try:
+ # Verify ownership
+ existing = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("*, campaign_deliverables!inner(campaign_id, campaigns!inner(brand_id))") \
+ .eq("id", metric_id) \
+ .execute()
+
+ if not existing.data:
+ raise HTTPException(status_code=404, detail="Metric not found")
+
+ if existing.data[0]["campaign_deliverables"]["campaigns"]["brand_id"] != brand["id"]:
+ raise HTTPException(status_code=403, detail="Not authorized")
+
+ # Update metric
+ update_data = {}
+ if metric_update.display_name is not None:
+ update_data["display_name"] = metric_update.display_name
+ if metric_update.target_value is not None:
+ update_data["target_value"] = metric_update.target_value
+
+ result = supabase_anon.table("campaign_deliverable_metrics") \
+ .update(update_data) \
+ .eq("id", metric_id) \
+ .execute()
+
+ if not result.data:
+ raise HTTPException(status_code=500, detail="Failed to update metric")
+
+ return MetricResponse(**result.data[0])
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error updating metric: {str(e)}")
+
+
+@router.delete("/analytics/metrics/{metric_id}", status_code=204)
+async def delete_metric(
+ metric_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """Delete a metric"""
+ try:
+ # Verify ownership
+ existing = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("*, campaign_deliverables(campaign_id)") \
+ .eq("id", metric_id) \
+ .execute()
+
+ if not existing.data:
+ raise HTTPException(status_code=404, detail="Metric not found")
+
+ campaign_id = existing.data[0]["campaign_deliverables"]["campaign_id"]
+ campaign_res = supabase_anon.table("campaigns") \
+ .select("brand_id") \
+ .eq("id", campaign_id) \
+ .execute()
+
+ if not campaign_res.data or campaign_res.data[0]["brand_id"] != brand["id"]:
+ raise HTTPException(status_code=403, detail="Not authorized")
+
+ supabase_anon.table("campaign_deliverable_metrics") \
+ .delete() \
+ .eq("id", metric_id) \
+ .execute()
+
+ return None
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error deleting metric: {str(e)}")
+
+
+@router.get("/analytics/campaigns/{campaign_id}/dashboard", response_model=AnalyticsDashboardResponse)
+async def get_analytics_dashboard(
+ campaign_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """Get analytics dashboard data for a campaign"""
+ try:
+ # Verify campaign ownership
+ campaign_res = supabase_anon.table("campaigns") \
+ .select("id, title, brand_id, platforms") \
+ .eq("id", campaign_id) \
+ .eq("brand_id", brand["id"]) \
+ .execute()
+
+ if not campaign_res.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ campaign = campaign_res.data[0]
+
+ # Get deliverables from campaign_deliverables table
+ deliverables_res = supabase_anon.table("campaign_deliverables") \
+ .select("id, platform, content_type") \
+ .eq("campaign_id", campaign_id) \
+ .execute()
+
+ deliverables = deliverables_res.data or []
+
+ # If no deliverables in table, check JSON field and migrate them
+ if not deliverables and campaign.get("deliverables"):
+ json_deliverables = campaign.get("deliverables", [])
+ if isinstance(json_deliverables, list) and len(json_deliverables) > 0:
+ # Migrate from JSON to table
+ deliverable_records = []
+ for deliverable in json_deliverables:
+ if isinstance(deliverable, dict):
+ deliverable_records.append({
+ "campaign_id": campaign_id,
+ "platform": deliverable.get("platform"),
+ "content_type": deliverable.get("content_type"),
+ "quantity": deliverable.get("quantity", 1),
+ "guidance": deliverable.get("guidance"),
+ "required": deliverable.get("required", True)
+ })
+
+ if deliverable_records:
+ migrated_res = supabase_anon.table("campaign_deliverables") \
+ .insert(deliverable_records) \
+ .execute()
+ deliverables = migrated_res.data or []
+
+ # Get metrics for each deliverable
+ deliverable_ids = [d["id"] for d in deliverables]
+ if deliverable_ids:
+ metrics_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("id, campaign_deliverable_id, name, display_name, target_value") \
+ .in_("campaign_deliverable_id", deliverable_ids) \
+ .execute()
+ metrics = metrics_res.data or []
+ else:
+ metrics = []
+
+ # Get updates count
+ metric_ids = [m["id"] for m in metrics]
+ if metric_ids:
+ updates_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("campaign_deliverable_metric_id") \
+ .in_("campaign_deliverable_metric_id", metric_ids) \
+ .execute()
+ updates = updates_res.data or []
+ else:
+ updates = []
+ metrics_with_updates = len(set(u["campaign_deliverable_metric_id"] for u in updates))
+
+ # Calculate overall progress
+ overall_progress = (metrics_with_updates / len(metrics)) * 100 if metrics else 0
+
+ # Group by platform
+ platform_data = {}
+ for deliverable in deliverables:
+ platform = deliverable.get("platform", "Unknown")
+ if platform not in platform_data:
+ platform_data[platform] = {
+ "platform": platform,
+ "deliverables": [],
+ "metrics": []
+ }
+ platform_data[platform]["deliverables"].append(deliverable)
+
+ for metric in metrics:
+ deliverable_id = metric["campaign_deliverable_id"]
+ deliverable = next((d for d in deliverables if d["id"] == deliverable_id), None)
+ if deliverable:
+ platform = deliverable.get("platform", "Unknown")
+ if platform in platform_data:
+ platform_data[platform]["metrics"].append(metric)
+
+ return AnalyticsDashboardResponse(
+ campaign_id=campaign_id,
+ campaign_title=campaign["title"],
+ platforms=list(platform_data.values()),
+ total_deliverables=len(deliverables),
+ total_metrics=len(metrics),
+ metrics_with_updates=metrics_with_updates,
+ overall_progress=round(overall_progress, 2)
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching dashboard: {str(e)}")
+
+
+@router.get("/analytics/brand/all-deliverables")
+async def get_all_brand_deliverables(
+ brand: dict = Depends(get_current_brand)
+):
+ """Get all deliverables across all campaigns for a brand with their metrics"""
+ try:
+ brand_id = brand["id"]
+
+ # Get all campaigns for this brand
+ campaigns_res = supabase_anon.table("campaigns") \
+ .select("id, title, status") \
+ .eq("brand_id", brand_id) \
+ .execute()
+
+ campaigns = campaigns_res.data or []
+ campaign_ids = [c["id"] for c in campaigns]
+
+ if not campaign_ids:
+ return {
+ "deliverables": [],
+ "campaigns": []
+ }
+
+ # Get all deliverables for these campaigns
+ deliverables_res = supabase_anon.table("campaign_deliverables") \
+ .select("id, campaign_id, platform, content_type, quantity, guidance, required, created_at") \
+ .in_("campaign_id", campaign_ids) \
+ .order("created_at", desc=True) \
+ .execute()
+
+ deliverables = deliverables_res.data or []
+
+ # Get metrics for each deliverable
+ deliverable_ids = [d["id"] for d in deliverables]
+ if deliverable_ids:
+ metrics_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("*") \
+ .in_("campaign_deliverable_id", deliverable_ids) \
+ .execute()
+ metrics = metrics_res.data or []
+ else:
+ metrics = []
+
+ # Get latest updates for each metric
+ metric_ids = [m["id"] for m in metrics]
+ if metric_ids:
+ updates_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("*") \
+ .in_("campaign_deliverable_metric_id", metric_ids) \
+ .order("submitted_at", desc=True) \
+ .execute()
+ updates = updates_res.data or []
+ else:
+ updates = []
+
+ # Get latest feedback for each update
+ update_ids = [u["id"] for u in updates]
+ if update_ids:
+ feedback_res = supabase_anon.table("campaign_deliverable_metric_feedback") \
+ .select("*") \
+ .in_("metric_update_id", update_ids) \
+ .order("created_at", desc=True) \
+ .execute()
+ feedback_list = feedback_res.data or []
+ else:
+ feedback_list = []
+
+ # Organize data: group updates by metric_id, get latest
+ updates_by_metric = {}
+ for update in updates:
+ metric_id = update["campaign_deliverable_metric_id"]
+ if metric_id not in updates_by_metric:
+ updates_by_metric[metric_id] = update
+
+ # Group feedback by update_id, get latest
+ feedback_by_update = {}
+ for feedback in feedback_list:
+ update_id = feedback["metric_update_id"]
+ if update_id not in feedback_by_update:
+ feedback_by_update[update_id] = feedback
+
+ # Attach latest update and feedback to each metric
+ for metric in metrics:
+ metric_id = metric["id"]
+ if metric_id in updates_by_metric:
+ metric["latest_update"] = updates_by_metric[metric_id]
+ update_id = updates_by_metric[metric_id]["id"]
+ if update_id in feedback_by_update:
+ metric["latest_feedback"] = feedback_by_update[update_id]
+ else:
+ metric["latest_update"] = None
+ metric["latest_feedback"] = None
+
+ # Attach metrics to deliverables
+ metrics_by_deliverable = {}
+ for metric in metrics:
+ deliverable_id = metric["campaign_deliverable_id"]
+ if deliverable_id not in metrics_by_deliverable:
+ metrics_by_deliverable[deliverable_id] = []
+ metrics_by_deliverable[deliverable_id].append(metric)
+
+ # Get creators for each campaign through contracts
+ creators_by_campaign = {}
+ for campaign_id in campaign_ids:
+ contracts_res = supabase_anon.table("contracts") \
+ .select("creator_id, proposals(campaign_id, creators(id, display_name))") \
+ .eq("brand_id", brand_id) \
+ .execute()
+
+ creators = []
+ for contract in contracts_res.data or []:
+ if contract.get("proposals"):
+ proposals = contract["proposals"] if isinstance(contract["proposals"], list) else [contract["proposals"]]
+ for proposal in proposals:
+ if proposal.get("campaign_id") == campaign_id and proposal.get("creators"):
+ creator = proposal["creators"]
+ if isinstance(creator, dict):
+ creators.append({
+ "id": creator.get("id"),
+ "display_name": creator.get("display_name", "Unknown Creator")
+ })
+ creators_by_campaign[campaign_id] = creators
+
+ # Attach campaign info, metrics, and creators to deliverables
+ campaigns_by_id = {c["id"]: c for c in campaigns}
+ for deliverable in deliverables:
+ campaign_id = deliverable["campaign_id"]
+ deliverable["campaign"] = campaigns_by_id.get(campaign_id, {})
+ deliverable["metrics"] = metrics_by_deliverable.get(deliverable["id"], [])
+ deliverable["campaign"]["creators"] = creators_by_campaign.get(campaign_id, [])
+
+ return {
+ "deliverables": deliverables,
+ "campaigns": campaigns,
+ "total_deliverables": len(deliverables),
+ "total_metrics": len(metrics),
+ "metrics_with_updates": len(updates_by_metric)
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching deliverables: {str(e)}")
+
+
+@router.get("/analytics/deliverables/{deliverable_id}/metrics")
+async def get_deliverable_metrics(
+ deliverable_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """Get all metrics for a deliverable with their latest updates"""
+ try:
+ # Verify ownership
+ deliverable_res = supabase_anon.table("campaign_deliverables") \
+ .select("id, campaign_id") \
+ .eq("id", deliverable_id) \
+ .execute()
+
+ if not deliverable_res.data:
+ raise HTTPException(status_code=404, detail="Deliverable not found")
+
+ campaign_id = deliverable_res.data[0]["campaign_id"]
+ campaign_res = supabase_anon.table("campaigns") \
+ .select("brand_id") \
+ .eq("id", campaign_id) \
+ .execute()
+
+ if not campaign_res.data or campaign_res.data[0]["brand_id"] != brand["id"]:
+ raise HTTPException(status_code=403, detail="Not authorized")
+
+ # Get metrics
+ metrics_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("*") \
+ .eq("campaign_deliverable_id", deliverable_id) \
+ .execute()
+
+ metrics = metrics_res.data or []
+
+ # Get latest updates for each metric
+ for metric in metrics:
+ updates_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("*") \
+ .eq("campaign_deliverable_metric_id", metric["id"]) \
+ .order("submitted_at", desc=True) \
+ .limit(1) \
+ .execute()
+
+ metric["latest_update"] = updates_res.data[0] if updates_res.data else None
+
+ # Get feedback for latest update
+ if metric["latest_update"]:
+ feedback_res = supabase_anon.table("campaign_deliverable_metric_feedback") \
+ .select("*") \
+ .eq("metric_update_id", metric["latest_update"]["id"]) \
+ .order("created_at", desc=True) \
+ .limit(1) \
+ .execute()
+
+ metric["latest_feedback"] = feedback_res.data[0] if feedback_res.data else None
+
+ return {"metrics": metrics}
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching metrics: {str(e)}")
+
+
+@router.post("/analytics/metric-updates/{update_id}/feedback", response_model=FeedbackResponse, status_code=201)
+async def create_feedback(
+ update_id: str,
+ feedback: FeedbackCreate,
+ brand: dict = Depends(get_current_brand)
+):
+ """Create feedback on a metric update"""
+ try:
+ # Verify update exists and belongs to brand's campaign
+ update_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("id, campaign_deliverable_metrics(campaign_deliverable_id)") \
+ .eq("id", update_id) \
+ .execute()
+
+ if not update_res.data:
+ raise HTTPException(status_code=404, detail="Metric update not found")
+
+ deliverable_id = update_res.data[0]["campaign_deliverable_metrics"]["campaign_deliverable_id"]
+ deliverable_res = supabase_anon.table("campaign_deliverables") \
+ .select("campaign_id") \
+ .eq("id", deliverable_id) \
+ .execute()
+
+ if not deliverable_res.data:
+ raise HTTPException(status_code=404, detail="Deliverable not found")
+
+ campaign_id = deliverable_res.data[0]["campaign_id"]
+ campaign_res = supabase_anon.table("campaigns") \
+ .select("brand_id") \
+ .eq("id", campaign_id) \
+ .execute()
+
+ if not campaign_res.data or campaign_res.data[0]["brand_id"] != brand["id"]:
+ raise HTTPException(status_code=403, detail="Not authorized")
+
+ # Create feedback
+ feedback_data = {
+ "metric_update_id": update_id,
+ "brand_id": brand["id"],
+ "feedback_text": feedback.feedback_text
+ }
+
+ result = supabase_anon.table("campaign_deliverable_metric_feedback") \
+ .insert(feedback_data) \
+ .execute()
+
+ if not result.data:
+ raise HTTPException(status_code=500, detail="Failed to create feedback")
+
+ return FeedbackResponse(**result.data[0])
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error creating feedback: {str(e)}")
+
+
+@router.post("/analytics/update-requests", response_model=UpdateRequestResponse, status_code=201)
+async def create_update_request(
+ request: UpdateRequestCreate,
+ brand: dict = Depends(get_current_brand)
+):
+ """Request a metric update from a creator"""
+ try:
+ # If specific metric, verify ownership
+ if request.campaign_deliverable_metric_id:
+ metric_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("*, campaign_deliverables(campaign_id)") \
+ .eq("id", request.campaign_deliverable_metric_id) \
+ .execute()
+
+ if not metric_res.data:
+ raise HTTPException(status_code=404, detail="Metric not found")
+
+ campaign_id = metric_res.data[0]["campaign_deliverables"]["campaign_id"]
+ campaign_res = supabase_anon.table("campaigns") \
+ .select("brand_id") \
+ .eq("id", campaign_id) \
+ .execute()
+
+ if not campaign_res.data or campaign_res.data[0]["brand_id"] != brand["id"]:
+ raise HTTPException(status_code=403, detail="Not authorized")
+
+ # Create update request
+ request_data = {
+ "campaign_deliverable_metric_id": request.campaign_deliverable_metric_id,
+ "brand_id": brand["id"],
+ "creator_id": request.creator_id,
+ "status": "pending"
+ }
+
+ result = supabase_anon.table("campaign_deliverable_metric_update_requests") \
+ .insert(request_data) \
+ .execute()
+
+ if not result.data:
+ raise HTTPException(status_code=500, detail="Failed to create update request")
+
+ return UpdateRequestResponse(**result.data[0])
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error creating update request: {str(e)}")
+
+
+# ==================== Creator Endpoints ====================
+
+@router.get("/analytics/creator/campaigns")
+async def get_creator_campaigns(
+ creator: dict = Depends(get_current_creator)
+):
+ """Get all campaigns that the creator is involved in with progress, value, status, and brand info"""
+ try:
+ creator_id = creator["id"]
+
+ # Get campaigns from contracts (through proposals)
+ contracts_res = supabase_anon.table("contracts") \
+ .select("id, status, terms, proposals(campaign_id, campaigns(*, brands(company_name, id)))") \
+ .eq("creator_id", creator_id) \
+ .execute()
+
+ campaign_ids = set()
+ campaigns_data = []
+ contract_by_campaign = {}
+
+ # Extract campaigns from contracts
+ for contract in contracts_res.data or []:
+ if contract.get("proposals"):
+ proposals = contract["proposals"] if isinstance(contract["proposals"], list) else [contract["proposals"]]
+ for proposal in proposals:
+ if proposal.get("campaigns") and proposal.get("campaign_id"):
+ campaign_id = proposal["campaign_id"]
+ if campaign_id not in campaign_ids:
+ campaign_ids.add(campaign_id)
+ campaign_data = proposal["campaigns"].copy()
+
+ # Add brand info
+ if campaign_data.get("brands"):
+ campaign_data["brand_name"] = campaign_data["brands"].get("company_name", "Unknown Brand")
+ campaign_data["brand_id"] = campaign_data["brands"].get("id")
+
+ # Add contract info
+ contract_terms = contract.get("terms", {})
+ if isinstance(contract_terms, dict):
+ campaign_data["value"] = contract_terms.get("amount") or contract_terms.get("proposed_amount") or 0
+ else:
+ campaign_data["value"] = 0
+
+ campaign_data["contract_id"] = contract["id"]
+ campaign_data["contract_status"] = contract.get("status", "unknown")
+
+ contract_by_campaign[campaign_id] = contract["id"]
+ campaigns_data.append(campaign_data)
+
+ # Also get campaigns from deals
+ deals_res = supabase_anon.table("deals") \
+ .select("campaign_id, agreed_amount, campaigns(*, brands(company_name, id))") \
+ .eq("creator_id", creator_id) \
+ .execute()
+
+ for deal in deals_res.data or []:
+ if deal.get("campaigns") and deal.get("campaign_id"):
+ campaign_id = deal["campaign_id"]
+ if campaign_id not in campaign_ids:
+ campaign_ids.add(campaign_id)
+ campaign_data = deal["campaigns"].copy()
+
+ if campaign_data.get("brands"):
+ campaign_data["brand_name"] = campaign_data["brands"].get("company_name", "Unknown Brand")
+ campaign_data["brand_id"] = campaign_data["brands"].get("id")
+
+ campaign_data["value"] = deal.get("agreed_amount", 0) or 0
+ campaigns_data.append(campaign_data)
+
+ # Calculate progress for each campaign
+ for campaign in campaigns_data:
+ campaign_id = campaign.get("id")
+ if not campaign_id:
+ campaign["progress"] = 0
+ continue
+
+ # Get contract deliverables for this campaign
+ contract_id = contract_by_campaign.get(campaign_id)
+ if contract_id:
+ # Get deliverables from contract_deliverables
+ contract_deliverables_res = supabase_anon.table("contract_deliverables") \
+ .select("id, status, campaign_deliverable_id") \
+ .eq("contract_id", contract_id) \
+ .execute()
+
+ contract_deliverables = contract_deliverables_res.data or []
+ total_deliverables = len(contract_deliverables)
+
+ if total_deliverables == 0:
+ campaign["progress"] = 0
+ else:
+ # Count completed deliverables
+ completed = sum(1 for d in contract_deliverables if d.get("status") == "completed")
+ campaign["progress"] = round((completed / total_deliverables) * 100, 1)
+ else:
+ campaign["progress"] = 0
+
+ return {"campaigns": campaigns_data}
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching campaigns: {str(e)}")
+
+
+@router.get("/analytics/creator/campaigns/{campaign_id}")
+async def get_creator_campaign_details(
+ campaign_id: str,
+ creator: dict = Depends(get_current_creator)
+):
+ """Get campaign details with deliverables grouped by platform"""
+ try:
+ creator_id = creator["id"]
+
+ # Verify creator has access to this campaign through a contract
+ contracts_res = supabase_anon.table("contracts") \
+ .select("id, status, terms, proposals(campaign_id)") \
+ .eq("creator_id", creator_id) \
+ .execute()
+
+ contract_id = None
+ has_access = False
+ for contract in contracts_res.data or []:
+ if contract.get("proposals"):
+ proposals = contract["proposals"] if isinstance(contract["proposals"], list) else [contract["proposals"]]
+ for proposal in proposals:
+ if proposal.get("campaign_id") == campaign_id:
+ contract_id = contract["id"]
+ has_access = True
+ break
+
+ if not has_access:
+ raise HTTPException(status_code=403, detail="You don't have access to this campaign")
+
+ # Get campaign details
+ campaign_res = supabase_anon.table("campaigns") \
+ .select("*, brands(company_name, id)") \
+ .eq("id", campaign_id) \
+ .single() \
+ .execute()
+
+ if not campaign_res.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ campaign = campaign_res.data.copy()
+ if campaign.get("brands"):
+ campaign["brand_name"] = campaign["brands"].get("company_name", "Unknown Brand")
+ campaign["brand_id"] = campaign["brands"].get("id")
+
+ # Get contract deliverables
+ contract_deliverables_res = supabase_anon.table("contract_deliverables") \
+ .select("*, campaign_deliverables(platform, content_type, quantity, guidance)") \
+ .eq("contract_id", contract_id) \
+ .execute()
+
+ contract_deliverables = contract_deliverables_res.data or []
+
+ # Group deliverables by platform
+ platforms = {}
+ for contract_deliv in contract_deliverables:
+ camp_deliv = contract_deliv.get("campaign_deliverables", {})
+ platform = camp_deliv.get("platform", "Unknown")
+
+ if platform not in platforms:
+ platforms[platform] = {
+ "platform": platform,
+ "deliverables": [],
+ "total": 0,
+ "completed": 0
+ }
+
+ # Calculate progress for this deliverable
+ status = contract_deliv.get("status", "pending")
+ if status == "completed":
+ platforms[platform]["completed"] += 1
+
+ platforms[platform]["total"] += 1
+
+ deliverable_data = {
+ "id": contract_deliv["id"],
+ "contract_deliverable_id": contract_deliv["id"],
+ "campaign_deliverable_id": contract_deliv.get("campaign_deliverable_id"),
+ "description": contract_deliv.get("description"),
+ "status": status,
+ "due_date": contract_deliv.get("due_date"),
+ "platform": platform,
+ "content_type": camp_deliv.get("content_type"),
+ "quantity": camp_deliv.get("quantity", 1),
+ "guidance": camp_deliv.get("guidance"),
+ }
+
+ platforms[platform]["deliverables"].append(deliverable_data)
+
+ # Calculate platform progress
+ for platform_data in platforms.values():
+ if platform_data["total"] > 0:
+ platform_data["progress"] = round((platform_data["completed"] / platform_data["total"]) * 100, 1)
+ else:
+ platform_data["progress"] = 0
+
+ campaign["platforms"] = list(platforms.values())
+ campaign["contract_id"] = contract_id
+
+ return campaign
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching campaign details: {str(e)}")
+
+
+@router.get("/analytics/creator/campaigns/{campaign_id}/platform/{platform}/deliverables")
+async def get_platform_deliverables(
+ campaign_id: str,
+ platform: str,
+ creator: dict = Depends(get_current_creator)
+):
+ """Get deliverables for a specific platform"""
+ try:
+ creator_id = creator["id"]
+
+ # Verify access
+ contracts_res = supabase_anon.table("contracts") \
+ .select("id, proposals(campaign_id)") \
+ .eq("creator_id", creator_id) \
+ .execute()
+
+ contract_id = None
+ for contract in contracts_res.data or []:
+ if contract.get("proposals"):
+ proposals = contract["proposals"] if isinstance(contract["proposals"], list) else [contract["proposals"]]
+ for proposal in proposals:
+ if proposal.get("campaign_id") == campaign_id:
+ contract_id = contract["id"]
+ break
+
+ if not contract_id:
+ raise HTTPException(status_code=403, detail="You don't have access to this campaign")
+
+ # Get deliverables for this platform
+ contract_deliverables_res = supabase_anon.table("contract_deliverables") \
+ .select("*, campaign_deliverables(platform, content_type, quantity, guidance)") \
+ .eq("contract_id", contract_id) \
+ .execute()
+
+ deliverables = []
+ for contract_deliv in contract_deliverables_res.data or []:
+ camp_deliv = contract_deliv.get("campaign_deliverables", {})
+ if camp_deliv.get("platform") == platform:
+ deliverable_data = {
+ "id": contract_deliv["id"],
+ "contract_deliverable_id": contract_deliv["id"],
+ "campaign_deliverable_id": contract_deliv.get("campaign_deliverable_id"),
+ "description": contract_deliv.get("description"),
+ "status": contract_deliv.get("status", "pending"),
+ "due_date": contract_deliv.get("due_date"),
+ "platform": platform,
+ "content_type": camp_deliv.get("content_type"),
+ "quantity": camp_deliv.get("quantity", 1),
+ "guidance": camp_deliv.get("guidance"),
+ }
+ deliverables.append(deliverable_data)
+
+ return {"deliverables": deliverables, "platform": platform}
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching platform deliverables: {str(e)}")
+
+
+@router.get("/analytics/creator/deliverables/{deliverable_id}/metrics")
+async def get_deliverable_metrics(
+ deliverable_id: str,
+ creator: dict = Depends(get_current_creator)
+):
+ """Get metrics for a contract deliverable"""
+ try:
+ creator_id = creator["id"]
+
+ # Get contract deliverable
+ contract_deliv_res = supabase_anon.table("contract_deliverables") \
+ .select("*, contracts(creator_id, proposals(campaign_id))") \
+ .eq("id", deliverable_id) \
+ .single() \
+ .execute()
+
+ if not contract_deliv_res.data:
+ raise HTTPException(status_code=404, detail="Deliverable not found")
+
+ contract_deliv = contract_deliv_res.data
+ contract = contract_deliv.get("contracts", {})
+
+ # Verify creator has access
+ if isinstance(contract, list) and len(contract) > 0:
+ contract = contract[0]
+
+ if contract.get("creator_id") != creator_id:
+ raise HTTPException(status_code=403, detail="You don't have access to this deliverable")
+
+ campaign_deliverable_id = contract_deliv.get("campaign_deliverable_id")
+ if not campaign_deliverable_id:
+ return {"metrics": []}
+
+ # Get metrics for the campaign deliverable
+ metrics_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("*") \
+ .eq("campaign_deliverable_id", campaign_deliverable_id) \
+ .execute()
+
+ metrics = metrics_res.data or []
+
+ # Get latest updates and feedback for each metric
+ for metric in metrics:
+ # Get latest update
+ updates_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("*") \
+ .eq("campaign_deliverable_metric_id", metric["id"]) \
+ .order("submitted_at", desc=True) \
+ .limit(1) \
+ .execute()
+
+ metric["latest_update"] = updates_res.data[0] if updates_res.data else None
+
+ # Get all feedback for this metric (through updates)
+ if metric["latest_update"]:
+ feedback_res = supabase_anon.table("campaign_deliverable_metric_feedback") \
+ .select("*, brands(company_name)") \
+ .eq("metric_update_id", metric["latest_update"]["id"]) \
+ .order("created_at", desc=True) \
+ .execute()
+
+ metric["feedback"] = feedback_res.data or []
+ else:
+ metric["feedback"] = []
+
+ return {"metrics": metrics, "deliverable": contract_deliv}
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching metrics: {str(e)}")
+
+
+@router.post("/analytics/creator/metrics/{metric_id}/comment")
+async def create_creator_comment(
+ metric_id: str,
+ comment: CreatorCommentCreate,
+ creator: dict = Depends(get_current_creator),
+ user: dict = Depends(get_current_user)
+):
+ """Creator adds a comment/response to a metric"""
+ try:
+ creator_id = creator["id"]
+
+ # Verify metric exists and creator has access
+ metric_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("*, campaign_deliverables(campaign_id)") \
+ .eq("id", metric_id) \
+ .execute()
+
+ if not metric_res.data:
+ raise HTTPException(status_code=404, detail="Metric not found")
+
+ metric = metric_res.data[0]
+ campaign_id = metric["campaign_deliverables"]["campaign_id"]
+
+ # Verify creator has access through contract
+ contracts_res = supabase_anon.table("contracts") \
+ .select("id, proposals(campaign_id)") \
+ .eq("creator_id", creator_id) \
+ .execute()
+
+ has_access = False
+ for contract in contracts_res.data or []:
+ if contract.get("proposals"):
+ proposals = contract["proposals"] if isinstance(contract["proposals"], list) else [contract["proposals"]]
+ for proposal in proposals:
+ if proposal.get("campaign_id") == campaign_id:
+ has_access = True
+ break
+
+ if not has_access:
+ raise HTTPException(status_code=403, detail="You don't have access to this metric")
+
+ # Get or create metric update for this comment
+ metric_update_id = comment.metric_update_id
+ if not metric_update_id:
+ # Get latest update or create a placeholder
+ updates_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("id") \
+ .eq("campaign_deliverable_metric_id", metric_id) \
+ .order("submitted_at", desc=True) \
+ .limit(1) \
+ .execute()
+
+ if updates_res.data:
+ metric_update_id = updates_res.data[0]["id"]
+ else:
+ # Create a placeholder update for the comment
+ update_data = {
+ "campaign_deliverable_metric_id": metric_id,
+ "value": 0, # Placeholder
+ "submitted_by": user["id"]
+ }
+ update_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .insert(update_data) \
+ .execute()
+ if update_res.data:
+ metric_update_id = update_res.data[0]["id"]
+ else:
+ raise HTTPException(status_code=500, detail="Failed to create metric update for comment")
+
+ # Store creator comment in the metric update's demographics field
+ # This allows us to track creator comments without modifying the feedback table schema
+ if metric_update_id:
+ update_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("demographics") \
+ .eq("id", metric_update_id) \
+ .single() \
+ .execute()
+
+ current_demographics = update_res.data.get("demographics", {}) if update_res.data else {}
+ if not isinstance(current_demographics, dict):
+ current_demographics = {}
+
+ if "creator_comments" not in current_demographics:
+ current_demographics["creator_comments"] = []
+
+ current_demographics["creator_comments"].append({
+ "text": comment.comment_text,
+ "created_by": creator_id,
+ "created_at": datetime.now(timezone.utc).isoformat()
+ })
+
+ supabase_anon.table("campaign_deliverable_metric_updates") \
+ .update({"demographics": current_demographics}) \
+ .eq("id", metric_update_id) \
+ .execute()
+
+ return {
+ "success": True,
+ "message": "Comment added successfully",
+ "metric_update_id": metric_update_id
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error adding comment: {str(e)}")
+
+
+@router.post("/analytics/metrics/{metric_id}/submit", response_model=MetricUpdateResponse, status_code=201)
+async def submit_metric_value(
+ metric_id: str,
+ submission: MetricValueSubmit,
+ creator: dict = Depends(get_current_creator),
+ user: dict = Depends(get_current_user)
+):
+ """Submit a metric value (manual entry)"""
+ try:
+ # Verify metric exists
+ metric_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("id") \
+ .eq("id", metric_id) \
+ .execute()
+
+ if not metric_res.data:
+ raise HTTPException(status_code=404, detail="Metric not found")
+
+ # Create update
+ update_data = {
+ "campaign_deliverable_metric_id": metric_id,
+ "value": submission.value,
+ "demographics": submission.demographics,
+ "screenshot_url": submission.screenshot_url,
+ "ai_extracted_data": submission.ai_extracted_data,
+ "submitted_by": user["id"]
+ }
+
+ result = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .insert(update_data) \
+ .execute()
+
+ if not result.data:
+ raise HTTPException(status_code=500, detail="Failed to submit metric value")
+
+ # Mark pending requests as completed
+ supabase_anon.table("campaign_deliverable_metric_update_requests") \
+ .update({"status": "completed"}) \
+ .eq("campaign_deliverable_metric_id", metric_id) \
+ .eq("creator_id", creator["id"]) \
+ .eq("status", "pending") \
+ .execute()
+
+ # Create audit log
+ audit_data = {
+ "campaign_deliverable_metric_id": metric_id,
+ "new_value": submission.value,
+ "changed_by": user["id"],
+ "change_reason": "Creator submitted metric value"
+ }
+
+ supabase_anon.table("campaign_deliverable_metric_audit") \
+ .insert(audit_data) \
+ .execute()
+
+ return MetricUpdateResponse(**result.data[0])
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error submitting metric value: {str(e)}")
+
+
+@router.post("/analytics/metrics/{metric_id}/extract-from-screenshot")
+async def extract_metrics_from_screenshot(
+ metric_id: str,
+ file: UploadFile = File(...),
+ creator: dict = Depends(get_current_creator)
+):
+ """Extract metrics from uploaded screenshot using AI"""
+ try:
+ # Verify metric exists
+ metric_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("id, name") \
+ .eq("id", metric_id) \
+ .execute()
+
+ if not metric_res.data:
+ raise HTTPException(status_code=404, detail="Metric not found")
+
+ # Read and encode image
+ image_bytes = await file.read()
+ image_base64 = base64.b64encode(image_bytes).decode('utf-8')
+
+ # Extract metrics using AI
+ extracted_data = await extract_metrics_from_image(image_base64)
+
+ return {
+ "extracted_data": extracted_data,
+ "metric_name": metric_res.data[0]["name"],
+ "suggested_value": extracted_data.get(metric_res.data[0]["name"]) or extracted_data.get("value")
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error extracting metrics: {str(e)}")
+
+
+@router.get("/analytics/creator/pending-requests")
+async def get_pending_update_requests(
+ creator: dict = Depends(get_current_creator)
+):
+ """Get all pending update requests for a creator"""
+ try:
+ requests_res = supabase_anon.table("campaign_deliverable_metric_update_requests") \
+ .select("*, campaign_deliverable_metrics(name, display_name, campaign_deliverable_id)") \
+ .eq("creator_id", creator["id"]) \
+ .eq("status", "pending") \
+ .execute()
+
+ # Enrich with deliverable and campaign info
+ for req in requests_res.data or []:
+ if req.get("campaign_deliverable_metrics"):
+ deliverable_id = req["campaign_deliverable_metrics"]["campaign_deliverable_id"]
+ deliverable_res = supabase_anon.table("campaign_deliverables") \
+ .select("platform, content_type, campaign_id") \
+ .eq("id", deliverable_id) \
+ .execute()
+ if deliverable_res.data:
+ req["campaign_deliverable_metrics"]["campaign_deliverables"] = deliverable_res.data[0]
+ campaign_id = deliverable_res.data[0]["campaign_id"]
+ campaign_res = supabase_anon.table("campaigns") \
+ .select("title") \
+ .eq("id", campaign_id) \
+ .execute()
+ if campaign_res.data:
+ req["campaign_deliverable_metrics"]["campaign_deliverables"]["campaigns"] = campaign_res.data[0]
+
+ return {"requests": requests_res.data or []}
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching requests: {str(e)}")
+
+
+@router.get("/analytics/metrics/{metric_id}/history")
+async def get_metric_history(
+ metric_id: str,
+ user: dict = Depends(get_current_user)
+):
+ """Get audit history for a metric"""
+ try:
+ # Verify metric exists
+ metric_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("*, campaign_deliverables(campaign_id)") \
+ .eq("id", metric_id) \
+ .execute()
+
+ if not metric_res.data:
+ raise HTTPException(status_code=404, detail="Metric not found")
+
+ # Get audit logs
+ audit_res = supabase_anon.table("campaign_deliverable_metric_audit") \
+ .select("*, profiles(name)") \
+ .eq("campaign_deliverable_metric_id", metric_id) \
+ .order("changed_at", desc=True) \
+ .execute()
+
+ # Get all updates
+ updates_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("*, profiles(name)") \
+ .eq("campaign_deliverable_metric_id", metric_id) \
+ .order("submitted_at", desc=True) \
+ .execute()
+
+ return {
+ "audit_logs": audit_res.data or [],
+ "updates": updates_res.data or []
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching history: {str(e)}")
+
+
+@router.get("/analytics/brand/dashboard-stats")
+async def get_brand_dashboard_stats(
+ brand: dict = Depends(get_current_brand)
+):
+ """Get comprehensive dashboard statistics for a brand"""
+ try:
+ brand_id = brand["id"]
+
+ # Get all campaigns
+ campaigns_res = supabase_anon.table("campaigns") \
+ .select("id, title, status, created_at, budget_min, budget_max") \
+ .eq("brand_id", brand_id) \
+ .execute()
+
+ campaigns = campaigns_res.data or []
+
+ # Calculate campaign stats
+ total_campaigns = len(campaigns)
+ active_campaigns = sum(1 for c in campaigns if c.get("status") == "active")
+ draft_campaigns = sum(1 for c in campaigns if c.get("status") == "draft")
+ completed_campaigns = sum(1 for c in campaigns if c.get("status") == "completed")
+
+ # Calculate total budget
+ total_budget = 0
+ for campaign in campaigns:
+ budget_max = campaign.get("budget_max") or 0
+ budget_min = campaign.get("budget_min") or 0
+ if budget_max > 0:
+ total_budget += budget_max
+ elif budget_min > 0:
+ total_budget += budget_min
+
+ # Get contracts and proposals
+ contracts_res = supabase_anon.table("contracts") \
+ .select("id, status, terms, proposals(campaign_id)") \
+ .eq("brand_id", brand_id) \
+ .execute()
+
+ contracts = contracts_res.data or []
+ total_contracts = len(contracts)
+ active_contracts = sum(1 for c in contracts if c.get("status") == "active")
+
+ # Get proposals through campaigns
+ proposals_res = supabase_anon.table("proposals") \
+ .select("id, status, campaign_id, campaigns!inner(brand_id)") \
+ .eq("campaigns.brand_id", brand_id) \
+ .execute()
+
+ proposals = proposals_res.data or []
+ total_proposals = len(proposals)
+ accepted_proposals = sum(1 for p in proposals if p.get("status") == "accepted")
+ pending_proposals = sum(1 for p in proposals if p.get("status") == "pending")
+
+ # Get deliverables
+ campaign_ids = [c["id"] for c in campaigns]
+ if campaign_ids:
+ deliverables_res = supabase_anon.table("campaign_deliverables") \
+ .select("id, platform, campaign_id") \
+ .in_("campaign_id", campaign_ids) \
+ .execute()
+ deliverables = deliverables_res.data or []
+ else:
+ deliverables = []
+
+ total_deliverables = len(deliverables)
+
+ # Get metrics and updates
+ deliverable_ids = [d["id"] for d in deliverables]
+ if deliverable_ids:
+ metrics_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("id, campaign_deliverable_id") \
+ .in_("campaign_deliverable_id", deliverable_ids) \
+ .execute()
+ metrics = metrics_res.data or []
+
+ metric_ids = [m["id"] for m in metrics]
+ if metric_ids:
+ updates_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("id, value, submitted_at, campaign_deliverable_metric_id") \
+ .in_("campaign_deliverable_metric_id", metric_ids) \
+ .order("submitted_at", desc=True) \
+ .execute()
+ updates = updates_res.data or []
+ else:
+ updates = []
+ else:
+ metrics = []
+ updates = []
+
+ total_metrics = len(metrics)
+ metrics_with_updates = len(set(u["campaign_deliverable_metric_id"] for u in updates))
+
+ # Calculate engagement metrics
+ total_engagement = sum(float(u.get("value", 0)) for u in updates)
+ avg_engagement = total_engagement / len(updates) if updates else 0
+
+ # Platform distribution
+ platform_counts = {}
+ for deliverable in deliverables:
+ platform = deliverable.get("platform", "Unknown")
+ platform_counts[platform] = platform_counts.get(platform, 0) + 1
+
+ # Campaign status distribution
+ status_distribution = {}
+ for campaign in campaigns:
+ status = campaign.get("status", "draft")
+ status_distribution[status] = status_distribution.get(status, 0) + 1
+
+ # Monthly campaign creation trend (last 6 months)
+ from datetime import timedelta
+ now = datetime.now(timezone.utc)
+ monthly_campaigns = {}
+ for i in range(6):
+ month_start = (now - timedelta(days=30*i)).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
+ month_key = month_start.strftime("%Y-%m")
+ monthly_campaigns[month_key] = 0
+
+ for campaign in campaigns:
+ created_at = campaign.get("created_at")
+ if created_at:
+ try:
+ if isinstance(created_at, str):
+ created_dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
+ else:
+ created_dt = created_at
+ month_key = created_dt.strftime("%Y-%m")
+ if month_key in monthly_campaigns:
+ monthly_campaigns[month_key] += 1
+ except:
+ pass
+
+ # Monthly engagement trend
+ monthly_engagement = {}
+ for i in range(6):
+ month_start = (now - timedelta(days=30*i)).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
+ month_key = month_start.strftime("%Y-%m")
+ monthly_engagement[month_key] = 0
+
+ for update in updates:
+ submitted_at = update.get("submitted_at")
+ if submitted_at:
+ try:
+ if isinstance(submitted_at, str):
+ submitted_dt = datetime.fromisoformat(submitted_at.replace('Z', '+00:00'))
+ else:
+ submitted_dt = submitted_at
+ month_key = submitted_dt.strftime("%Y-%m")
+ if month_key in monthly_engagement:
+ monthly_engagement[month_key] += float(update.get("value", 0))
+ except:
+ pass
+
+ return {
+ "overview": {
+ "total_campaigns": total_campaigns,
+ "active_campaigns": active_campaigns,
+ "draft_campaigns": draft_campaigns,
+ "completed_campaigns": completed_campaigns,
+ "total_contracts": total_contracts,
+ "active_contracts": active_contracts,
+ "total_proposals": total_proposals,
+ "accepted_proposals": accepted_proposals,
+ "pending_proposals": pending_proposals,
+ "total_deliverables": total_deliverables,
+ "total_metrics": total_metrics,
+ "metrics_with_updates": metrics_with_updates,
+ "total_budget": round(total_budget, 2),
+ "total_engagement": round(total_engagement, 2),
+ "avg_engagement": round(avg_engagement, 2)
+ },
+ "platform_distribution": platform_counts,
+ "status_distribution": status_distribution,
+ "monthly_campaigns": monthly_campaigns,
+ "monthly_engagement": monthly_engagement
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching dashboard stats: {str(e)}")
+
+
+@router.get("/analytics/creator/dashboard-stats")
+async def get_creator_dashboard_stats(
+ creator: dict = Depends(get_current_creator)
+):
+ """Get comprehensive dashboard statistics for a creator"""
+ try:
+ creator_id = creator["id"]
+
+ # Get campaigns from contracts
+ contracts_res = supabase_anon.table("contracts") \
+ .select("id, status, terms, proposals(campaign_id, campaigns(*))") \
+ .eq("creator_id", creator_id) \
+ .execute()
+
+ campaign_ids = set()
+ campaigns_data = []
+ total_earnings = 0
+
+ for contract in contracts_res.data or []:
+ if contract.get("proposals"):
+ proposals = contract["proposals"] if isinstance(contract["proposals"], list) else [contract["proposals"]]
+ for proposal in proposals:
+ if proposal.get("campaigns") and proposal.get("campaign_id"):
+ campaign_id = proposal["campaign_id"]
+ if campaign_id not in campaign_ids:
+ campaign_ids.add(campaign_id)
+ campaign_data = proposal["campaigns"].copy()
+ campaigns_data.append(campaign_data)
+
+ # Calculate earnings from contract
+ contract_terms = contract.get("terms", {})
+ if isinstance(contract_terms, dict):
+ amount = contract_terms.get("amount") or contract_terms.get("proposed_amount") or 0
+ total_earnings += float(amount) if amount else 0
+
+ # Also get from deals
+ deals_res = supabase_anon.table("deals") \
+ .select("campaign_id, agreed_amount, campaigns(*)") \
+ .eq("creator_id", creator_id) \
+ .execute()
+
+ for deal in deals_res.data or []:
+ if deal.get("campaigns") and deal.get("campaign_id"):
+ campaign_id = deal["campaign_id"]
+ if campaign_id not in campaign_ids:
+ campaign_ids.add(campaign_id)
+ campaign_data = deal["campaigns"].copy()
+ campaigns_data.append(campaign_data)
+ total_earnings += float(deal.get("agreed_amount", 0) or 0)
+
+ total_campaigns = len(campaigns_data)
+ active_campaigns = sum(1 for c in campaigns_data if c.get("status") == "active")
+ completed_campaigns = sum(1 for c in campaigns_data if c.get("status") == "completed")
+
+ # Get contract deliverables
+ contract_ids = [c["id"] for c in contracts_res.data or []]
+ if contract_ids:
+ deliverables_res = supabase_anon.table("contract_deliverables") \
+ .select("id, status, campaign_deliverable_id") \
+ .in_("contract_id", contract_ids) \
+ .execute()
+ deliverables = deliverables_res.data or []
+ else:
+ deliverables = []
+
+ total_deliverables = len(deliverables)
+ completed_deliverables = sum(1 for d in deliverables if d.get("status") == "completed")
+ pending_deliverables = sum(1 for d in deliverables if d.get("status") == "pending")
+
+ # Get proposals
+ proposals_res = supabase_anon.table("proposals") \
+ .select("id, status, campaign_id") \
+ .eq("creator_id", creator_id) \
+ .execute()
+
+ proposals = proposals_res.data or []
+ total_proposals = len(proposals)
+ accepted_proposals = sum(1 for p in proposals if p.get("status") == "accepted")
+ pending_proposals = sum(1 for p in proposals if p.get("status") == "pending")
+ rejected_proposals = sum(1 for p in proposals if p.get("status") == "rejected")
+
+ # Get metrics and updates
+ deliverable_ids = [d.get("campaign_deliverable_id") for d in deliverables if d.get("campaign_deliverable_id")]
+ if deliverable_ids:
+ metrics_res = supabase_anon.table("campaign_deliverable_metrics") \
+ .select("id, campaign_deliverable_id") \
+ .in_("campaign_deliverable_id", deliverable_ids) \
+ .execute()
+ metrics = metrics_res.data or []
+
+ metric_ids = [m["id"] for m in metrics]
+ if metric_ids:
+ updates_res = supabase_anon.table("campaign_deliverable_metric_updates") \
+ .select("id, value, submitted_at, campaign_deliverable_metric_id") \
+ .eq("submitted_by", creator_id) \
+ .in_("campaign_deliverable_metric_id", metric_ids) \
+ .order("submitted_at", desc=True) \
+ .execute()
+ updates = updates_res.data or []
+ else:
+ updates = []
+ else:
+ metrics = []
+ updates = []
+
+ total_metrics = len(metrics)
+ metrics_submitted = len(set(u["campaign_deliverable_metric_id"] for u in updates))
+
+ # Calculate engagement metrics
+ total_engagement = sum(float(u.get("value", 0)) for u in updates)
+ avg_engagement = total_engagement / len(updates) if updates else 0
+
+ # Platform distribution from deliverables
+ platform_counts = {}
+ if deliverable_ids:
+ campaign_deliverables_res = supabase_anon.table("campaign_deliverables") \
+ .select("id, platform") \
+ .in_("id", deliverable_ids) \
+ .execute()
+ for deliv in campaign_deliverables_res.data or []:
+ platform = deliv.get("platform", "Unknown")
+ platform_counts[platform] = platform_counts.get(platform, 0) + 1
+
+ # Campaign status distribution
+ status_distribution = {}
+ for campaign in campaigns_data:
+ status = campaign.get("status", "draft")
+ status_distribution[status] = status_distribution.get(status, 0) + 1
+
+ # Monthly earnings trend (last 6 months)
+ from datetime import timedelta
+ now = datetime.now(timezone.utc)
+ monthly_earnings = {}
+ for i in range(6):
+ month_start = (now - timedelta(days=30*i)).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
+ month_key = month_start.strftime("%Y-%m")
+ monthly_earnings[month_key] = 0
+
+ # Calculate monthly earnings from contracts
+ for contract in contracts_res.data or []:
+ created_at = contract.get("created_at")
+ if created_at:
+ try:
+ if isinstance(created_at, str):
+ created_dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
+ else:
+ created_dt = created_at
+ month_key = created_dt.strftime("%Y-%m")
+ if month_key in monthly_earnings:
+ contract_terms = contract.get("terms", {})
+ if isinstance(contract_terms, dict):
+ amount = contract_terms.get("amount") or contract_terms.get("proposed_amount") or 0
+ monthly_earnings[month_key] += float(amount) if amount else 0
+ except:
+ pass
+
+ # Monthly engagement trend
+ monthly_engagement = {}
+ for i in range(6):
+ month_start = (now - timedelta(days=30*i)).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
+ month_key = month_start.strftime("%Y-%m")
+ monthly_engagement[month_key] = 0
+
+ for update in updates:
+ submitted_at = update.get("submitted_at")
+ if submitted_at:
+ try:
+ if isinstance(submitted_at, str):
+ submitted_dt = datetime.fromisoformat(submitted_at.replace('Z', '+00:00'))
+ else:
+ submitted_dt = submitted_at
+ month_key = submitted_dt.strftime("%Y-%m")
+ if month_key in monthly_engagement:
+ monthly_engagement[month_key] += float(update.get("value", 0))
+ except:
+ pass
+
+ # Deliverable status distribution
+ deliverable_status = {}
+ for deliverable in deliverables:
+ status = deliverable.get("status", "pending")
+ deliverable_status[status] = deliverable_status.get(status, 0) + 1
+
+ return {
+ "overview": {
+ "total_campaigns": total_campaigns,
+ "active_campaigns": active_campaigns,
+ "completed_campaigns": completed_campaigns,
+ "total_proposals": total_proposals,
+ "accepted_proposals": accepted_proposals,
+ "pending_proposals": pending_proposals,
+ "rejected_proposals": rejected_proposals,
+ "total_deliverables": total_deliverables,
+ "completed_deliverables": completed_deliverables,
+ "pending_deliverables": pending_deliverables,
+ "total_metrics": total_metrics,
+ "metrics_submitted": metrics_submitted,
+ "total_earnings": round(total_earnings, 2),
+ "total_engagement": round(total_engagement, 2),
+ "avg_engagement": round(avg_engagement, 2)
+ },
+ "platform_distribution": platform_counts,
+ "status_distribution": status_distribution,
+ "deliverable_status": deliverable_status,
+ "monthly_earnings": monthly_earnings,
+ "monthly_engagement": monthly_engagement
+ }
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error fetching dashboard stats: {str(e)}")
+
diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py
new file mode 100644
index 0000000..ae910f0
--- /dev/null
+++ b/backend/app/api/routes/auth.py
@@ -0,0 +1,165 @@
+from fastapi import APIRouter, HTTPException
+from pydantic import BaseModel, EmailStr, Field
+from gotrue.errors import AuthApiError
+from typing import Optional
+
+from app.core.supabase_clients import supabase_anon, supabase_admin
+
+router = APIRouter()
+
+
+class SignupRequest(BaseModel):
+ """
+ Request schema for user signup.
+ """
+ name: str = Field(..., min_length=2)
+ email: EmailStr
+ password: str = Field(..., min_length=8)
+ role: str = Field(..., pattern="^(Creator|Brand)$")
+
+
+class SignupResponse(BaseModel):
+ """
+ Response schema for user signup.
+ """
+ message: str
+ user_id: Optional[str] = None
+
+
+@router.post("/api/auth/signup", response_model=SignupResponse)
+async def signup_user(payload: SignupRequest):
+ """
+ Atomic signup using Supabase Admin API:
+ 1. Create auth user via admin.create_user()
+ 2. Insert profile row with id = created user id
+ 3. If profile insert fails -> delete auth user (rollback)
+ """
+ try:
+ # 1. Create auth user using admin API (atomic)
+ try:
+ create_res = supabase_admin.auth.admin.create_user({
+ "email": payload.email,
+ "password": payload.password,
+ # TEMP: Auto-confirm email for development/testing
+ "email_confirm": True
+ })
+ except AuthApiError as e:
+ status = 409 if getattr(e, "code", None) == "user_already_exists" else getattr(e, "status", 400) or 400
+ raise HTTPException(status_code=status, detail=str(e)) from e
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Admin create_user failed: {str(e)}") from e
+
+ # Handle different response shapes from supabase-py
+ user = None
+ if hasattr(create_res, "user"):
+ user = create_res.user
+ elif hasattr(create_res, "data") and create_res.data:
+ if hasattr(create_res.data, "user"):
+ user = create_res.data.user
+ elif isinstance(create_res.data, dict) and "user" in create_res.data:
+ user = create_res.data["user"]
+ elif isinstance(create_res, dict):
+ user = create_res.get("user") or create_res.get("data", {}).get("user")
+
+ if not user:
+ raise HTTPException(status_code=500, detail="Failed to create auth user (admin API).")
+
+ user_id = getattr(user, "id", None) or (user.get("id") if hasattr(user, "get") else None)
+ if not user_id:
+ raise HTTPException(status_code=500, detail="Auth user created but no id returned.")
+
+
+ # 2. Insert profile row (with rollback on any failure)
+ profile = {
+ "id": user_id,
+ "name": payload.name,
+ "role": payload.role
+ }
+ try:
+ res = supabase_admin.table("profiles").insert(profile).execute()
+ insert_data = getattr(res, "data", None)
+ if not insert_data:
+ raise Exception("No data returned from profile insert.")
+ except Exception as insert_exc:
+ # Always attempt rollback if insert fails
+ try:
+ supabase_admin.auth.admin.delete_user(user_id)
+ except Exception as rollback_err:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Profile insert failed and rollback deletion failed for user {user_id}: {rollback_err}"
+ ) from rollback_err
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to create profile. Auth user removed for safety. Reason: {insert_exc}"
+ ) from insert_exc
+
+ return SignupResponse(
+ message="Signup successful! Check your email for verification link.",
+ user_id=user_id
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Signup failed: {str(e)}") from e
+
+# ------------------- LOGIN ROUTE -------------------
+class LoginRequest(BaseModel):
+ """
+ Request schema for user login.
+ """
+ email: EmailStr
+ password: str = Field(..., min_length=8)
+
+class LoginResponse(BaseModel):
+ """
+ Response schema for user login.
+ """
+ message: str
+ user_id: Optional[str] = None
+ email: Optional[str] = None
+ role: Optional[str] = None
+ name: Optional[str] = None
+ onboarding_completed: bool = False
+
+@router.post("/api/auth/login", response_model=LoginResponse)
+async def login_user(payload: LoginRequest):
+ """
+ Login route: authenticates user and enforces email verification.
+ If email is not verified, returns 403 with a helpful message.
+ Includes user profile info in response.
+ """
+ try:
+ # 1. Authenticate user
+ try:
+ auth_resp = supabase_anon.auth.sign_in_with_password({
+ "email": payload.email,
+ "password": payload.password
+ })
+ user = getattr(auth_resp, "user", None)
+ except Exception as e:
+ # Supabase Python SDK v2 raises exceptions for auth errors
+ if hasattr(e, "code") and e.code == "email_not_confirmed":
+ raise HTTPException(status_code=403, detail="Please verify your email before logging in.")
+ raise HTTPException(status_code=401, detail=str(e))
+ if not user or not getattr(user, "id", None):
+ raise HTTPException(status_code=401, detail="Invalid credentials.")
+
+ # 2. Fetch user profile
+ profile_res = supabase_admin.table("profiles").select("id, name, role, onboarding_completed").eq("id", user.id).single().execute()
+ profile = profile_res.data if hasattr(profile_res, "data") else None
+ if not profile:
+ raise HTTPException(status_code=404, detail="User profile not found.")
+
+ return LoginResponse(
+ message="Login successful.",
+ user_id=user.id,
+ email=user.email,
+ role=profile.get("role"),
+ name=profile.get("name"),
+ onboarding_completed=profile.get("onboarding_completed", False)
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Login failed: {str(e)}") from e
diff --git a/backend/app/api/routes/campaigns.py b/backend/app/api/routes/campaigns.py
new file mode 100644
index 0000000..fb6ff5d
--- /dev/null
+++ b/backend/app/api/routes/campaigns.py
@@ -0,0 +1,1540 @@
+"""
+Campaign management routes for brand users.
+"""
+from fastapi import APIRouter, HTTPException, Depends, Query
+from pydantic import BaseModel, Field
+from typing import Optional, List
+from datetime import datetime, timezone
+from app.core.supabase_clients import supabase_anon
+from app.core.dependencies import get_current_brand, get_current_creator, optional_security
+from fastapi.security import HTTPAuthorizationCredentials
+from uuid import UUID
+
+router = APIRouter()
+
+
+class CampaignCreate(BaseModel):
+ """Schema for creating a new campaign."""
+ title: str = Field(..., min_length=1, max_length=255)
+ slug: Optional[str] = None
+ short_description: Optional[str] = None
+ description: Optional[str] = None
+ status: str = Field(default="draft", pattern="^(draft|active|paused|completed|archived)$")
+ platforms: List[str] = Field(default_factory=list)
+ deliverables: Optional[List[dict]] = Field(default_factory=list)
+ target_audience: Optional[dict] = Field(default_factory=dict)
+ budget_min: Optional[float] = None
+ budget_max: Optional[float] = None
+ preferred_creator_niches: List[str] = Field(default_factory=list)
+ preferred_creator_followers_range: Optional[str] = None
+ starts_at: Optional[datetime] = None
+ ends_at: Optional[datetime] = None
+ is_featured: bool = False
+ is_open_for_applications: bool = False
+ is_on_campaign_wall: bool = False
+
+
+class CampaignUpdate(BaseModel):
+ """Schema for updating an existing campaign."""
+ title: Optional[str] = None
+ slug: Optional[str] = None
+ short_description: Optional[str] = None
+ description: Optional[str] = None
+ status: Optional[str] = None
+ platforms: Optional[List[str]] = None
+ deliverables: Optional[List[dict]] = None
+ target_audience: Optional[dict] = None
+ budget_min: Optional[float] = None
+ budget_max: Optional[float] = None
+ preferred_creator_niches: Optional[List[str]] = None
+ preferred_creator_followers_range: Optional[str] = None
+ starts_at: Optional[datetime] = None
+ ends_at: Optional[datetime] = None
+ is_featured: Optional[bool] = None
+ is_open_for_applications: Optional[bool] = None
+ is_on_campaign_wall: Optional[bool] = None
+
+
+class CampaignResponse(BaseModel):
+ """Schema for campaign response."""
+ id: str
+ brand_id: str
+ title: str
+ slug: Optional[str]
+ short_description: Optional[str]
+ description: Optional[str]
+ status: str
+ platforms: List[str]
+ deliverables: List[dict]
+ target_audience: dict
+ budget_min: Optional[float]
+ budget_max: Optional[float]
+ preferred_creator_niches: List[str]
+ preferred_creator_followers_range: Optional[str]
+ created_at: datetime
+ updated_at: datetime
+ published_at: Optional[datetime]
+ starts_at: Optional[datetime]
+ ends_at: Optional[datetime]
+ is_featured: bool
+ is_open_for_applications: Optional[bool] = False
+ is_on_campaign_wall: Optional[bool] = False
+
+
+@router.post("/campaigns", response_model=CampaignResponse, status_code=201)
+async def create_campaign(campaign: CampaignCreate, brand: dict = Depends(get_current_brand)):
+ """
+ Create a new campaign for a brand.
+
+ - **campaign**: Campaign details matching the database schema
+ """
+ supabase = supabase_anon
+
+ # Get brand ID from authenticated brand profile
+ brand_id = brand['id']
+
+ # Generate slug if not provided
+ if not campaign.slug:
+ import re
+ slug = re.sub(r'[^a-z0-9]+', '-', campaign.title.lower()).strip('-')
+ # Ensure uniqueness by checking existing slugs (race condition handled below)
+ base_slug = f"{slug}-{datetime.now(timezone.utc).strftime('%Y%m%d')}"
+ campaign.slug = base_slug
+ counter = 1
+ while True:
+ existing = supabase.table("campaigns").select("id").eq("slug", campaign.slug).execute()
+ if not existing.data:
+ break
+ campaign.slug = f"{base_slug}-{counter}"
+ counter += 1
+
+ import time
+ max_attempts = 5
+ for attempt in range(max_attempts):
+ try:
+ # Prepare campaign data
+ campaign_data = {
+ "brand_id": brand_id,
+ "title": campaign.title,
+ "slug": campaign.slug,
+ "short_description": campaign.short_description,
+ "description": campaign.description,
+ "status": campaign.status,
+ "platforms": campaign.platforms,
+ "deliverables": campaign.deliverables,
+ "target_audience": campaign.target_audience,
+ "budget_min": campaign.budget_min,
+ "budget_max": campaign.budget_max,
+ "preferred_creator_niches": campaign.preferred_creator_niches,
+ "preferred_creator_followers_range": campaign.preferred_creator_followers_range,
+ "starts_at": campaign.starts_at.isoformat() if campaign.starts_at else None,
+ "ends_at": campaign.ends_at.isoformat() if campaign.ends_at else None,
+ "is_featured": campaign.is_featured,
+ "is_open_for_applications": campaign.is_open_for_applications,
+ "is_on_campaign_wall": campaign.is_on_campaign_wall,
+ }
+
+ # If status is active, set published_at
+ if campaign.status == "active":
+ campaign_data["published_at"] = datetime.now(timezone.utc).isoformat()
+
+ # Insert campaign
+ response = supabase.table("campaigns").insert(campaign_data).execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to create campaign")
+
+ created_campaign = response.data[0]
+ campaign_id = created_campaign["id"]
+
+ # Also insert deliverables into campaign_deliverables table for analytics
+ if campaign.deliverables:
+ deliverable_records = []
+ for deliverable in campaign.deliverables:
+ if isinstance(deliverable, dict):
+ deliverable_records.append({
+ "campaign_id": campaign_id,
+ "platform": deliverable.get("platform"),
+ "content_type": deliverable.get("content_type"),
+ "quantity": deliverable.get("quantity", 1),
+ "guidance": deliverable.get("guidance"),
+ "required": deliverable.get("required", True)
+ })
+
+ if deliverable_records:
+ supabase.table("campaign_deliverables").insert(deliverable_records).execute()
+
+ return created_campaign
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ # Check for unique constraint violation on slug
+ if "duplicate key value violates unique constraint" in str(e) and "slug" in str(e):
+ # Regenerate slug and retry
+ campaign.slug = f"{base_slug}-{int(time.time() * 1000)}"
+ continue
+ raise HTTPException(status_code=500, detail=f"Error creating campaign: {str(e)}") from e
+ raise HTTPException(status_code=500, detail="Could not generate a unique slug for the campaign after multiple attempts.")
+
+
+
+@router.get("/campaigns", response_model=List[CampaignResponse])
+async def get_campaigns(
+ brand: dict = Depends(get_current_brand),
+ status: Optional[str] = Query(None, description="Filter by status"),
+ search: Optional[str] = Query(None, description="Search by title or description"),
+ platform: Optional[str] = Query(None, description="Filter by platform"),
+ budget_min: Optional[float] = Query(None, description="Minimum budget"),
+ budget_max: Optional[float] = Query(None, description="Maximum budget"),
+ starts_after: Optional[datetime] = Query(None, description="Campaign starts after this date"),
+ ends_before: Optional[datetime] = Query(None, description="Campaign ends before this date"),
+ limit: int = Query(50, ge=1, le=100),
+ offset: int = Query(0, ge=0)
+):
+ """
+ Get all campaigns for a brand with optional filters.
+
+ - **status**: Optional filter by campaign status
+ - **search**: Optional search term for title or description
+ - **platform**: Optional filter by platform
+ - **budget_min**: Optional minimum budget
+ - **budget_max**: Optional maximum budget
+ - **starts_after**: Optional filter for campaigns starting after this date
+ - **ends_before**: Optional filter for campaigns ending before this date
+ - **limit**: Maximum number of results (default: 50, max: 100)
+ - **offset**: Number of results to skip for pagination
+ """
+ supabase = supabase_anon
+
+ # Get brand ID from authenticated brand profile
+ brand_id = brand['id']
+
+ try:
+ # Build query
+ query = supabase.table("campaigns").select("*").eq("brand_id", brand_id)
+
+ # Apply filters
+ if status:
+ query = query.eq("status", status)
+
+ if search:
+ # Search in title and description
+ query = query.or_(f"title.ilike.%{search}%,description.ilike.%{search}%")
+
+ if platform:
+ query = query.contains("platforms", [platform])
+
+ if budget_min is not None:
+ query = query.gte("budget_min", budget_min)
+
+ if budget_max is not None:
+ query = query.lte("budget_max", budget_max)
+
+ if starts_after:
+ query = query.gte("starts_at", starts_after.isoformat())
+
+ if ends_before:
+ query = query.lte("ends_at", ends_before.isoformat())
+
+ # Apply pagination and ordering
+ query = query.order("created_at", desc=True).range(offset, offset + limit - 1)
+
+ response = query.execute()
+
+ return response.data if response.data else []
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ # Log the full error internally
+ import logging
+ logger = logging.getLogger(__name__)
+ logger.exception("Error fetching campaigns")
+ raise HTTPException(
+ status_code=500,
+ detail="Error fetching campaigns. Please contact support if the issue persists."
+ ) from e
+
+
+# ============================================================================
+# PUBLIC CAMPAIGN WALL ENDPOINT (Must be before /campaigns/{campaign_id} to avoid route conflict)
+# ============================================================================
+
+@router.get("/campaigns/public", response_model=List[CampaignResponse])
+async def get_public_campaigns(
+ search: Optional[str] = Query(None, description="Search by title or description"),
+ platform: Optional[str] = Query(None, description="Filter by platform"),
+ niche: Optional[str] = Query(None, description="Filter by preferred niche"),
+ budget_min: Optional[float] = Query(None, description="Minimum budget"),
+ budget_max: Optional[float] = Query(None, description="Maximum budget"),
+ limit: int = Query(50, ge=1, le=100),
+ offset: int = Query(0, ge=0)
+):
+ """
+ Get all campaigns that are open for applications and on the campaign wall.
+ This endpoint is accessible to any authenticated user (creators and brands).
+ """
+ from app.core.supabase_clients import supabase_anon
+ supabase = supabase_anon
+
+ try:
+ # Build query for campaigns that are open and on wall
+ query = supabase.table("campaigns").select("*").eq("is_open_for_applications", True).eq("is_on_campaign_wall", True).eq("status", "active")
+
+ # Apply filters
+ if search:
+ query = query.or_(f"title.ilike.%{search}%,description.ilike.%{search}%,short_description.ilike.%{search}%")
+
+ if platform:
+ query = query.contains("platforms", [platform])
+
+ if niche:
+ query = query.contains("preferred_creator_niches", [niche])
+
+ if budget_min is not None:
+ query = query.gte("budget_min", budget_min)
+
+ if budget_max is not None:
+ query = query.lte("budget_max", budget_max)
+
+ # Apply pagination and ordering
+ query = query.order("created_at", desc=True).range(offset, offset + limit - 1)
+
+ response = query.execute()
+
+ return response.data if response.data else []
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching public campaigns: {str(e)}"
+ ) from e
+
+
+@router.get("/campaigns/{campaign_id}", response_model=CampaignResponse)
+async def get_campaign(
+ campaign_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Get a single campaign by ID.
+
+ - **campaign_id**: The campaign ID
+ """
+ supabase = supabase_anon
+
+ # Get brand ID from authenticated brand profile
+ brand_id = brand['id']
+
+ try:
+ # Fetch campaign and verify ownership
+ response = supabase.table("campaigns").select("*").eq("id", campaign_id).eq("brand_id", brand_id).single().execute()
+
+ if not response.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ return response.data
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ if "PGRST116" in str(e): # No rows returned
+ raise HTTPException(status_code=404, detail="Campaign not found") from e
+ raise HTTPException(status_code=500, detail=f"Error fetching campaign: {str(e)}") from e
+
+
+@router.get("/campaigns/{campaign_id}/deliverables")
+async def get_campaign_deliverables(
+ campaign_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Get all deliverables for a campaign.
+
+ - **campaign_id**: The campaign ID
+ """
+ supabase = supabase_anon
+ brand_id = brand['id']
+
+ try:
+ # Verify campaign exists and belongs to this brand
+ campaign_res = supabase.table("campaigns") \
+ .select("id, deliverables") \
+ .eq("id", campaign_id) \
+ .eq("brand_id", brand_id) \
+ .single() \
+ .execute()
+
+ if not campaign_res.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ campaign = campaign_res.data
+
+ # Get deliverables from table
+ deliverables_res = supabase.table("campaign_deliverables") \
+ .select("*") \
+ .eq("campaign_id", campaign_id) \
+ .order("created_at", desc=False) \
+ .execute()
+
+ deliverables = deliverables_res.data or []
+
+ # If no deliverables in table, check JSON field and migrate them
+ if not deliverables and campaign.get("deliverables"):
+ json_deliverables = campaign.get("deliverables", [])
+ if isinstance(json_deliverables, list) and len(json_deliverables) > 0:
+ # Migrate from JSON to table
+ deliverable_records = []
+ for deliverable in json_deliverables:
+ if isinstance(deliverable, dict):
+ deliverable_records.append({
+ "campaign_id": campaign_id,
+ "platform": deliverable.get("platform"),
+ "content_type": deliverable.get("content_type"),
+ "quantity": deliverable.get("quantity", 1),
+ "guidance": deliverable.get("guidance"),
+ "required": deliverable.get("required", True)
+ })
+
+ if deliverable_records:
+ migrated_res = supabase.table("campaign_deliverables") \
+ .insert(deliverable_records) \
+ .execute()
+ deliverables = migrated_res.data or []
+
+ return {"deliverables": deliverables}
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching deliverables: {str(e)}"
+ ) from e
+
+
+@router.put("/campaigns/{campaign_id}", response_model=CampaignResponse)
+async def update_campaign(
+ campaign_id: str,
+ campaign: CampaignUpdate,
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Update an existing campaign.
+
+ - **campaign_id**: The campaign ID
+ - **campaign**: Updated campaign details
+ """
+ supabase = supabase_anon
+
+ # Get brand ID from authenticated brand profile
+ brand_id = brand['id']
+
+ try:
+ # Verify campaign exists and belongs to this brand
+ existing = supabase.table("campaigns").select("id, published_at").eq("id", campaign_id).eq("brand_id", brand_id).single().execute()
+
+ if not existing.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ # Prepare update data (only include non-None fields) using Pydantic v2 API
+ update_data = campaign.model_dump(exclude_none=True)
+
+ if not update_data:
+ raise HTTPException(status_code=400, detail="No fields to update")
+
+ # Update timestamp
+ update_data["updated_at"] = datetime.now(timezone.utc).isoformat()
+
+ # Serialize datetime fields to ISO format
+ if "starts_at" in update_data and update_data["starts_at"] is not None:
+ if isinstance(update_data["starts_at"], datetime):
+ update_data["starts_at"] = update_data["starts_at"].isoformat()
+ if "ends_at" in update_data and update_data["ends_at"] is not None:
+ if isinstance(update_data["ends_at"], datetime):
+ update_data["ends_at"] = update_data["ends_at"].isoformat()
+
+ # If status changes to active and published_at is not set, set it
+ if update_data.get("status") == "active" and not existing.data.get("published_at"):
+ update_data["published_at"] = datetime.now(timezone.utc).isoformat()
+
+ # Update campaign
+ response = supabase.table("campaigns").update(update_data).eq("id", campaign_id).execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to update campaign")
+
+ return response.data[0]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ if "PGRST116" in str(e):
+ raise HTTPException(status_code=404, detail="Campaign not found") from e
+ raise HTTPException(status_code=500, detail=f"Error updating campaign: {str(e)}") from e
+
+
+@router.delete("/campaigns/{campaign_id}", status_code=204)
+async def delete_campaign(
+ campaign_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Delete a campaign.
+
+ - **campaign_id**: The campaign ID
+ """
+ supabase = supabase_anon
+
+ # Get brand ID from authenticated brand profile
+ brand_id = brand['id']
+
+ try:
+ # Verify campaign exists and belongs to this brand
+ existing = supabase.table("campaigns").select("id").eq("id", campaign_id).eq("brand_id", brand_id).single().execute()
+
+ if not existing.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ # Delete campaign
+ supabase.table("campaigns").delete().eq("id", campaign_id).execute()
+
+ return None
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ if "PGRST116" in str(e):
+ raise HTTPException(status_code=404, detail="Campaign not found") from e
+ raise HTTPException(status_code=500, detail=f"Error deleting campaign: {str(e)}") from e
+
+
+@router.get("/campaigns/{campaign_id}/find-creators")
+async def find_matching_creators(
+ campaign_id: str,
+ brand: dict = Depends(get_current_brand),
+ limit: int = Query(4, ge=1, le=10),
+ use_ai: bool = Query(True, description="Use Groq to find and rank creators")
+):
+ """
+ Find matching creators for a campaign using AI (Groq).
+ Returns top matching creators with match scores and reasoning.
+ """
+ from app.core.config import settings
+ from groq import Groq
+ import json
+
+ supabase = supabase_anon
+ brand_id = brand['id']
+
+ try:
+ # Fetch campaign and verify ownership
+ campaign_resp = supabase.table("campaigns") \
+ .select("*") \
+ .eq("id", campaign_id) \
+ .eq("brand_id", brand_id) \
+ .single() \
+ .execute()
+
+ if not campaign_resp.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ campaign = campaign_resp.data
+
+ # Fetch brand details
+ brand_resp = supabase.table("brands") \
+ .select("*") \
+ .eq("id", brand_id) \
+ .single() \
+ .execute()
+
+ brand_data = brand_resp.data if brand_resp.data else {}
+
+ # Fetch active creators
+ creators_resp = supabase.table("creators") \
+ .select("*") \
+ .eq("is_active", True) \
+ .order("total_followers", desc=True) \
+ .limit(200) \
+ .execute()
+
+ candidates = creators_resp.data or []
+
+ if not candidates:
+ return []
+
+ # Initial rule-based filtering and scoring
+ def list_overlap(a, b):
+ if not a or not b:
+ return 0
+ return len(set(a or []).intersection(set(b or [])))
+
+ def followers_in_range(creator_followers, range_str):
+ if not range_str or not creator_followers:
+ return True
+ range_str = range_str.lower()
+ if "nano" in range_str:
+ return creator_followers < 10000
+ elif "micro" in range_str:
+ return 10000 <= creator_followers < 100000
+ elif "mid" in range_str:
+ return 100000 <= creator_followers < 1000000
+ elif "macro" in range_str or "mega" in range_str:
+ return creator_followers >= 1000000
+ return True
+
+ scored = []
+ campaign_niches = campaign.get("preferred_creator_niches", []) or []
+ campaign_platforms = campaign.get("platforms", []) or []
+
+ for creator in candidates:
+ score = 0.0
+ reasons = []
+
+ # Niche match (30 points)
+ creator_niches = [creator.get("primary_niche")] + (creator.get("secondary_niches") or [])
+ niche_overlap = list_overlap(campaign_niches, creator_niches)
+ if niche_overlap > 0:
+ niche_score = min(30.0, niche_overlap * 15.0)
+ score += niche_score
+ reasons.append(f"Matches {niche_overlap} preferred niche(s)")
+
+ # Platform match (25 points)
+ creator_platforms = []
+ if creator.get("youtube_handle"): creator_platforms.append("YouTube")
+ if creator.get("instagram_handle"): creator_platforms.append("Instagram")
+ if creator.get("tiktok_handle"): creator_platforms.append("TikTok")
+ if creator.get("twitter_handle"): creator_platforms.append("Twitter")
+ platform_overlap = list_overlap(campaign_platforms, creator_platforms)
+ if platform_overlap > 0:
+ platform_score = min(25.0, platform_overlap * 12.5)
+ score += platform_score
+ reasons.append(f"Active on {platform_overlap} required platform(s)")
+
+ # Follower range (15 points)
+ if followers_in_range(creator.get("total_followers", 0), campaign.get("preferred_creator_followers_range")):
+ score += 15.0
+ reasons.append("Follower count in desired range")
+
+ # Engagement rate (15 points)
+ engagement = creator.get("engagement_rate") or 0
+ if engagement > 3.0:
+ score += 15.0
+ reasons.append("High engagement rate")
+ elif engagement > 1.5:
+ score += 8.0
+ reasons.append("Good engagement rate")
+
+ # Content type alignment (10 points)
+ campaign_deliverables = campaign.get("deliverables", []) or []
+ content_types_needed = [d.get("content_type") for d in campaign_deliverables if isinstance(d, dict)]
+ creator_content_types = creator.get("content_types", []) or []
+ content_overlap = list_overlap(content_types_needed, creator_content_types)
+ if content_overlap > 0:
+ score += min(10.0, content_overlap * 5.0)
+ reasons.append("Content type alignment")
+
+ # Experience (5 points)
+ years_exp = creator.get("years_of_experience", 0) or 0
+ if years_exp >= 3:
+ score += 5.0
+ reasons.append("Experienced creator")
+
+ if score > 0:
+ scored.append((creator, score, ", ".join(reasons) or "Potential match"))
+
+ # Sort by score
+ scored.sort(key=lambda x: x[1], reverse=True)
+
+ # Take top candidates for AI ranking
+ top_candidates = scored[:max(12, limit * 3)]
+
+ # Use AI to refine ranking and generate better reasons
+ if use_ai and settings.groq_api_key and top_candidates:
+ try:
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ # Build prompt
+ campaign_summary = f"""
+Campaign: {campaign.get('title', 'N/A')}
+Description: {campaign.get('description', campaign.get('short_description', 'N/A'))}
+Platforms: {', '.join(campaign_platforms)}
+Preferred Niches: {', '.join(campaign_niches)}
+Budget: {campaign.get('budget_min', 0)} - {campaign.get('budget_max', 0)} INR
+Follower Range: {campaign.get('preferred_creator_followers_range', 'Any')}
+"""
+
+ brand_summary = f"""
+Brand: {brand_data.get('company_name', 'N/A')}
+Industry: {brand_data.get('industry', 'N/A')}
+Brand Values: {', '.join(brand_data.get('brand_values', []) or [])}
+"""
+
+ candidates_info = []
+ for creator, score, reason in top_candidates:
+ candidates_info.append(f"""
+Creator: {creator.get('display_name', 'N/A')}
+ID: {creator.get('id')}
+Niche: {creator.get('primary_niche', 'N/A')}
+Followers: {creator.get('total_followers', 0)}
+Engagement: {creator.get('engagement_rate', 0)}%
+Platforms: {', '.join(creator_platforms)}
+Bio: {creator.get('bio', 'N/A')[:200]}
+Current Score: {score}
+Current Reason: {reason}
+""")
+
+ prompt = f"""You are an expert at matching brands with content creators for marketing campaigns.
+
+{campaign_summary}
+
+{brand_summary}
+
+CANDIDATE CREATORS:
+{''.join(candidates_info)}
+
+Analyze which creators are the BEST matches for this campaign. Consider:
+1. Niche alignment with campaign requirements
+2. Platform presence matching campaign needs
+3. Audience fit with brand target audience
+4. Content style compatibility
+5. Engagement quality
+6. Professionalism and experience
+
+For each creator, provide:
+- A refined match score (0-100)
+- Detailed reasoning explaining why they fit (or don't fit) the campaign
+- Specific strengths that make them a good match
+
+Return JSON array with this structure:
+[
+ {{
+ "id": "creator_id_here",
+ "match_score": 85,
+ "reasoning": "Detailed explanation of why this creator is a good match for the campaign"
+ }},
+ ...
+]
+
+Return ONLY the JSON array, no additional text."""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {"role": "system", "content": "Return only valid JSON array."},
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.5,
+ max_completion_tokens=2000,
+ top_p=1,
+ stream=False,
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "[]"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ try:
+ ai_rankings = json.loads(content)
+ ai_map = {item.get("id"): (item.get("match_score", 0), item.get("reasoning", "")) for item in ai_rankings if item.get("id")}
+
+ # Update scores and reasons
+ refined = []
+ for creator, old_score, old_reason in top_candidates:
+ creator_id = creator.get("id")
+ if creator_id in ai_map:
+ new_score, new_reason = ai_map[creator_id]
+ refined.append((creator, float(new_score), new_reason or old_reason))
+ else:
+ refined.append((creator, old_score, old_reason))
+
+ scored = refined
+ scored.sort(key=lambda x: x[1], reverse=True)
+ except Exception:
+ # If AI parsing fails, continue with rule-based scores
+ pass
+
+ except Exception:
+ # If AI fails, continue with rule-based ranking
+ pass
+
+ # Return top results
+ results = []
+ for creator, score, reason in scored[:limit]:
+ platforms = []
+ if creator.get("youtube_handle"): platforms.append("YouTube")
+ if creator.get("instagram_handle"): platforms.append("Instagram")
+ if creator.get("tiktok_handle"): platforms.append("TikTok")
+ if creator.get("twitter_handle"): platforms.append("Twitter")
+
+ results.append({
+ "id": creator["id"],
+ "display_name": creator.get("display_name", "Unknown"),
+ "tagline": creator.get("tagline"),
+ "bio": creator.get("bio"),
+ "profile_picture_url": creator.get("profile_picture_url"),
+ "primary_niche": creator.get("primary_niche"),
+ "secondary_niches": creator.get("secondary_niches") or [],
+ "total_followers": creator.get("total_followers", 0),
+ "engagement_rate": creator.get("engagement_rate"),
+ "top_platforms": platforms[:3],
+ "match_score": round(score, 2),
+ "match_reasoning": reason,
+ # Include full creator data for expanded view
+ "full_details": creator
+ })
+
+ return results
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error finding creators: {str(e)}"
+ ) from e
+
+
+@router.get("/campaigns/{campaign_id}/search-creator")
+async def search_creator_by_id_or_name(
+ campaign_id: str,
+ query: str = Query(..., description="Creator ID (UUID) or display name"),
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Search for a creator by ID or name for sending proposals.
+ Returns creator details if found.
+ """
+ supabase = supabase_anon
+ brand_id = brand['id']
+
+ try:
+ # Verify campaign belongs to brand
+ campaign_resp = supabase.table("campaigns") \
+ .select("id") \
+ .eq("id", campaign_id) \
+ .eq("brand_id", brand_id) \
+ .single() \
+ .execute()
+
+ if not campaign_resp.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ # Try to find creator by ID first (UUID format)
+ creator = None
+ search_query = query.strip()
+
+ # Check if it looks like a UUID
+ import re
+ uuid_pattern = re.compile(
+ r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
+ re.IGNORECASE
+ )
+
+ if uuid_pattern.match(search_query):
+ # Search by ID
+ creator_resp = supabase.table("creators") \
+ .select("*") \
+ .eq("id", search_query) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ if creator_resp.data:
+ creator = creator_resp.data
+
+ # If not found by ID, search by name
+ if not creator:
+ # Search by display name (case-insensitive partial match)
+ creators_resp = supabase.table("creators") \
+ .select("*") \
+ .eq("is_active", True) \
+ .ilike("display_name", f"%{search_query}%") \
+ .limit(10) \
+ .execute()
+
+ creators = creators_resp.data or []
+
+ # Find exact match first, then partial matches
+ exact_match = next(
+ (c for c in creators if c.get("display_name", "").lower() == search_query.lower()),
+ None
+ )
+
+ if exact_match:
+ creator = exact_match
+ elif creators:
+ # Return first match if multiple found
+ creator = creators[0]
+ if len(creators) > 1:
+ # Return list of matches for user to choose
+ return {
+ "found": True,
+ "multiple_matches": True,
+ "creators": [
+ {
+ "id": c["id"],
+ "display_name": c.get("display_name", "Unknown"),
+ "tagline": c.get("tagline"),
+ "profile_picture_url": c.get("profile_picture_url"),
+ "primary_niche": c.get("primary_niche"),
+ "total_followers": c.get("total_followers", 0),
+ "engagement_rate": c.get("engagement_rate"),
+ }
+ for c in creators[:5] # Limit to 5 matches
+ ]
+ }
+
+ if not creator:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Creator not found with ID or name: {search_query}"
+ )
+
+ # Format response similar to find-creators endpoint
+ platforms = []
+ if creator.get("youtube_handle"): platforms.append("YouTube")
+ if creator.get("instagram_handle"): platforms.append("Instagram")
+ if creator.get("tiktok_handle"): platforms.append("TikTok")
+ if creator.get("twitter_handle"): platforms.append("Twitter")
+
+ return {
+ "id": creator["id"],
+ "display_name": creator.get("display_name", "Unknown"),
+ "tagline": creator.get("tagline"),
+ "bio": creator.get("bio"),
+ "profile_picture_url": creator.get("profile_picture_url"),
+ "primary_niche": creator.get("primary_niche"),
+ "secondary_niches": creator.get("secondary_niches") or [],
+ "total_followers": creator.get("total_followers", 0),
+ "engagement_rate": creator.get("engagement_rate"),
+ "top_platforms": platforms[:3],
+ "full_details": creator,
+ "found": True,
+ "multiple_matches": False
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error searching creator: {str(e)}"
+ ) from e
+
+
+# ============================================================================
+# CAMPAIGN WALL APPLICATION ROUTES (For Creators)
+# ============================================================================
+
+class CampaignApplicationCreate(BaseModel):
+ """Schema for creating a campaign application."""
+ payment_min: Optional[float] = None
+ payment_max: Optional[float] = None
+ timeline_days: Optional[int] = None
+ timeline_weeks: Optional[int] = None
+ description: str = Field(..., min_length=1)
+
+
+class CampaignApplicationResponse(BaseModel):
+ """Schema for campaign application response."""
+ id: str
+ campaign_id: str
+ creator_id: str
+ payment_min: Optional[float]
+ payment_max: Optional[float]
+ timeline_days: Optional[int]
+ timeline_weeks: Optional[int]
+ description: Optional[str]
+ message: Optional[str]
+ proposed_amount: Optional[float]
+ status: str
+ created_at: datetime
+ updated_at: datetime
+ creator_name: Optional[str] = None
+ creator_profile_picture: Optional[str] = None
+ campaign_title: Optional[str] = None
+
+
+# ============================================================================
+# CAMPAIGN WALL RECOMMENDATIONS (Moved here to keep all campaign wall routes together)
+# ============================================================================
+
+@router.get("/creators/campaign-wall/recommendations", response_model=List[CampaignResponse])
+async def get_campaign_recommendations(
+ creator: dict = Depends(get_current_creator),
+ limit: int = Query(10, ge=1, le=20),
+ use_ai: bool = Query(True, description="Use Gemini to recommend campaigns")
+):
+ """
+ Get AI-recommended campaigns for the current creator based on their profile.
+ """
+ from app.core.config import settings
+ import json
+
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Fetch creator profile
+ creator_resp = supabase.table("creators") \
+ .select("*") \
+ .eq("id", creator_id) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ if not creator_resp.data:
+ raise HTTPException(status_code=404, detail="Creator profile not found")
+
+ creator_data = creator_resp.data
+
+ # Fetch all open campaigns
+ campaigns_resp = supabase.table("campaigns") \
+ .select("*") \
+ .eq("is_open_for_applications", True) \
+ .eq("is_on_campaign_wall", True) \
+ .eq("status", "active") \
+ .order("created_at", desc=True) \
+ .limit(100) \
+ .execute()
+
+ campaigns = campaigns_resp.data or []
+
+ if not campaigns:
+ return []
+
+ # Use AI to rank campaigns if enabled
+ if use_ai and settings.gemini_api_key:
+ try:
+ import google.generativeai as genai
+ genai.configure(api_key=settings.gemini_api_key)
+
+ creator_summary = f"""
+Creator Profile:
+- Name: {creator_data.get('display_name', 'N/A')}
+- Primary Niche: {creator_data.get('primary_niche', 'N/A')}
+- Secondary Niches: {', '.join(creator_data.get('secondary_niches', []) or [])}
+- Total Followers: {creator_data.get('total_followers', 0)}
+- Engagement Rate: {creator_data.get('engagement_rate', 0)}%
+- Bio: {creator_data.get('bio', 'N/A')[:300]}
+- Platforms: {', '.join([p for p in ['YouTube', 'Instagram', 'TikTok', 'Twitter'] if creator_data.get(f'{p.lower()}_handle')])}
+- Content Types: {', '.join(creator_data.get('content_types', []) or [])}
+"""
+
+ campaigns_info = []
+ for idx, campaign in enumerate(campaigns[:50]): # Limit to 50 for AI processing
+ campaigns_info.append(f"""
+Campaign {idx + 1}:
+- ID: {campaign.get('id')}
+- Title: {campaign.get('title', 'N/A')}
+- Description: {campaign.get('description', campaign.get('short_description', 'N/A'))[:200]}
+- Platforms: {', '.join(campaign.get('platforms', []) or [])}
+- Preferred Niches: {', '.join(campaign.get('preferred_creator_niches', []) or [])}
+- Budget: {campaign.get('budget_min', 0)} - {campaign.get('budget_max', 0)} INR
+- Follower Range: {campaign.get('preferred_creator_followers_range', 'Any')}
+""")
+
+ prompt = f"""You are an expert at matching content creators with marketing campaigns.
+
+{creator_summary}
+
+AVAILABLE CAMPAIGNS:
+{''.join(campaigns_info)}
+
+Analyze which campaigns are the BEST matches for this creator. Consider:
+1. Niche alignment
+2. Platform compatibility
+3. Audience fit
+4. Budget alignment
+5. Content style match
+
+Return a JSON array with campaign IDs ranked by match quality (best first):
+[
+ {{"id": "campaign_id_1", "match_score": 95, "reasoning": "Why this is a great match"}},
+ {{"id": "campaign_id_2", "match_score": 88, "reasoning": "Why this is a good match"}},
+ ...
+]
+
+Return ONLY the JSON array, no additional text."""
+
+ model = genai.GenerativeModel('gemini-pro')
+ response = model.generate_content(prompt)
+ content = response.text.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ try:
+ ai_rankings = json.loads(content)
+ ai_map = {item.get("id"): (item.get("match_score", 0), item.get("reasoning", "")) for item in ai_rankings if item.get("id")}
+
+ # Sort campaigns by AI score
+ ranked_campaigns = []
+ for campaign in campaigns:
+ campaign_id = campaign.get("id")
+ if campaign_id in ai_map:
+ score, reasoning = ai_map[campaign_id]
+ ranked_campaigns.append((campaign, score, reasoning))
+
+ ranked_campaigns.sort(key=lambda x: x[1], reverse=True)
+ return [camp for camp, _, _ in ranked_campaigns[:limit]]
+
+ except Exception:
+ # If AI parsing fails, return campaigns by date
+ pass
+
+ except Exception:
+ # If AI fails, continue with date-based ordering
+ pass
+
+ # Return campaigns ordered by creation date
+ return campaigns[:limit]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error getting campaign recommendations: {str(e)}"
+ ) from e
+
+
+@router.post("/campaigns/{campaign_id}/applications", response_model=CampaignApplicationResponse, status_code=201)
+async def create_campaign_application(
+ campaign_id: str,
+ application: CampaignApplicationCreate,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Create a new application for a campaign.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Verify campaign exists and is open for applications
+ campaign_resp = supabase.table("campaigns") \
+ .select("*") \
+ .eq("id", campaign_id) \
+ .eq("is_open_for_applications", True) \
+ .eq("is_on_campaign_wall", True) \
+ .eq("status", "active") \
+ .single() \
+ .execute()
+
+ if not campaign_resp.data:
+ raise HTTPException(status_code=404, detail="Campaign not found or not open for applications")
+
+ # Check if creator already applied
+ existing = supabase.table("campaign_applications") \
+ .select("id") \
+ .eq("campaign_id", campaign_id) \
+ .eq("creator_id", creator_id) \
+ .execute()
+
+ if existing.data:
+ raise HTTPException(
+ status_code=400,
+ detail="You have already applied to this campaign"
+ )
+
+ # Get creator profile snapshot
+ creator_resp = supabase.table("creators") \
+ .select("*") \
+ .eq("id", creator_id) \
+ .single() \
+ .execute()
+
+ creator_profile = creator_resp.data if creator_resp.data else {}
+
+ # Create application
+ application_data = {
+ "campaign_id": campaign_id,
+ "creator_id": creator_id,
+ "payment_min": application.payment_min,
+ "payment_max": application.payment_max,
+ "timeline_days": application.timeline_days,
+ "timeline_weeks": application.timeline_weeks,
+ "description": application.description,
+ "message": application.description, # Use description as message
+ "proposed_amount": application.payment_max or application.payment_min,
+ "profile_snapshot": creator_profile,
+ "status": "applied"
+ }
+
+ response = supabase.table("campaign_applications").insert(application_data).execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to create application")
+
+ # Fetch with creator and campaign info
+ app = response.data[0]
+ app["creator_name"] = creator_profile.get("display_name")
+ app["creator_profile_picture"] = creator_profile.get("profile_picture_url")
+ app["campaign_title"] = campaign_resp.data.get("title")
+
+ return app
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error creating application: {str(e)}"
+ ) from e
+
+
+@router.get("/campaigns/{campaign_id}/applications", response_model=List[CampaignApplicationResponse])
+async def get_campaign_applications(
+ campaign_id: str,
+ brand: dict = Depends(get_current_brand),
+ status: Optional[str] = Query(None, description="Filter by application status")
+):
+ """
+ Get all applications for a campaign. Only the brand owner can view applications.
+ """
+ supabase = supabase_anon
+ brand_id = brand['id']
+
+ try:
+ # Verify campaign belongs to brand
+ campaign_resp = supabase.table("campaigns") \
+ .select("id, title") \
+ .eq("id", campaign_id) \
+ .eq("brand_id", brand_id) \
+ .single() \
+ .execute()
+
+ if not campaign_resp.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ # Build query
+ query = supabase.table("campaign_applications") \
+ .select("*, creators!campaign_applications_creator_id_fkey(display_name, profile_picture_url)") \
+ .eq("campaign_id", campaign_id) \
+ .order("created_at", desc=True)
+
+ if status:
+ query = query.eq("status", status)
+
+ response = query.execute()
+
+ applications = response.data or []
+
+ # Format response
+ formatted = []
+ for app in applications:
+ creator_info = app.get("creators", {}) if isinstance(app.get("creators"), dict) else {}
+ formatted.append({
+ "id": app["id"],
+ "campaign_id": app["campaign_id"],
+ "creator_id": app["creator_id"],
+ "payment_min": app.get("payment_min"),
+ "payment_max": app.get("payment_max"),
+ "timeline_days": app.get("timeline_days"),
+ "timeline_weeks": app.get("timeline_weeks"),
+ "description": app.get("description"),
+ "message": app.get("message"),
+ "proposed_amount": app.get("proposed_amount"),
+ "status": app.get("status", "applied"),
+ "created_at": app["created_at"],
+ "updated_at": app["updated_at"],
+ "creator_name": creator_info.get("display_name") if creator_info else None,
+ "creator_profile_picture": creator_info.get("profile_picture_url") if creator_info else None,
+ "campaign_title": campaign_resp.data.get("title")
+ })
+
+ return formatted
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching applications: {str(e)}"
+ ) from e
+
+
+@router.put("/campaigns/{campaign_id}/applications/{application_id}/status")
+async def update_application_status(
+ campaign_id: str,
+ application_id: str,
+ new_status: str = Query(..., description="New status: 'reviewing', 'accepted', 'rejected'"),
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Update the status of a campaign application. Only the brand owner can update.
+ """
+ supabase = supabase_anon
+ brand_id = brand['id']
+
+ try:
+ # Verify campaign belongs to brand
+ campaign_resp = supabase.table("campaigns") \
+ .select("id") \
+ .eq("id", campaign_id) \
+ .eq("brand_id", brand_id) \
+ .single() \
+ .execute()
+
+ if not campaign_resp.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ # Verify application exists and belongs to campaign
+ app_resp = supabase.table("campaign_applications") \
+ .select("id, campaign_id, creator_id, status") \
+ .eq("id", application_id) \
+ .eq("campaign_id", campaign_id) \
+ .single() \
+ .execute()
+
+ if not app_resp.data:
+ raise HTTPException(status_code=404, detail="Application not found")
+
+ # Validate status
+ # The enum should have: 'applied', 'reviewing', 'accepted', 'rejected'
+ valid_statuses = ["reviewing", "accepted", "rejected"]
+ if new_status not in valid_statuses:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Invalid status. Must be one of: {', '.join(valid_statuses)}"
+ )
+
+ # Update status
+ update_data = {
+ "status": new_status,
+ "updated_at": datetime.now(timezone.utc).isoformat()
+ }
+
+ # Update the application
+ update_response = supabase.table("campaign_applications") \
+ .update(update_data) \
+ .eq("id", application_id) \
+ .execute()
+
+ if not update_response.data:
+ raise HTTPException(status_code=500, detail="Failed to update application status")
+
+ # Fetch the updated application with only necessary fields to avoid JSON serialization issues
+ # Use a simple select without profile_snapshot or attachments
+ try:
+ response = supabase.table("campaign_applications") \
+ .select("id, campaign_id, creator_id, payment_min, payment_max, timeline_days, timeline_weeks, description, message, proposed_amount, status, created_at, updated_at") \
+ .eq("id", application_id) \
+ .single() \
+ .execute()
+ except Exception as e:
+ # If select fails, try to get minimal data
+ response = supabase.table("campaign_applications") \
+ .select("id, campaign_id, creator_id, status, created_at, updated_at") \
+ .eq("id", application_id) \
+ .single() \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to fetch updated application")
+
+ # Fetch creator info separately
+ creator_resp = supabase.table("creators") \
+ .select("display_name, profile_picture_url") \
+ .eq("id", response.data["creator_id"]) \
+ .single() \
+ .execute()
+
+ creator_info = creator_resp.data if creator_resp.data else {}
+ campaign_title_resp = supabase.table("campaigns") \
+ .select("title") \
+ .eq("id", campaign_id) \
+ .single() \
+ .execute()
+
+ # Build result manually to ensure clean JSON
+ result = {
+ "id": response.data.get("id"),
+ "campaign_id": response.data.get("campaign_id"),
+ "creator_id": response.data.get("creator_id"),
+ "payment_min": response.data.get("payment_min"),
+ "payment_max": response.data.get("payment_max"),
+ "timeline_days": response.data.get("timeline_days"),
+ "timeline_weeks": response.data.get("timeline_weeks"),
+ "description": response.data.get("description"),
+ "message": response.data.get("message"),
+ "proposed_amount": response.data.get("proposed_amount"),
+ "status": response.data.get("status"),
+ "created_at": response.data.get("created_at"),
+ "updated_at": response.data.get("updated_at"),
+ "creator_name": creator_info.get("display_name"),
+ "creator_profile_picture": creator_info.get("profile_picture_url"),
+ "campaign_title": campaign_title_resp.data.get("title") if campaign_title_resp.data else None
+ }
+
+ return result
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error updating application status: {str(e)}"
+ ) from e
+
+
+@router.post("/campaigns/{campaign_id}/applications/{application_id}/create-proposal")
+async def create_proposal_from_application(
+ campaign_id: str,
+ application_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Create a proposal from an accepted application. This creates a proposal with status 'pending' for negotiation.
+ """
+ supabase = supabase_anon
+ brand_id = brand['id']
+
+ try:
+ # Verify campaign belongs to brand
+ campaign_resp = supabase.table("campaigns") \
+ .select("*") \
+ .eq("id", campaign_id) \
+ .eq("brand_id", brand_id) \
+ .single() \
+ .execute()
+
+ if not campaign_resp.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ # Verify application exists, belongs to campaign, and is accepted
+ app_resp = supabase.table("campaign_applications") \
+ .select("*") \
+ .eq("id", application_id) \
+ .eq("campaign_id", campaign_id) \
+ .eq("status", "accepted") \
+ .single() \
+ .execute()
+
+ if not app_resp.data:
+ raise HTTPException(
+ status_code=404,
+ detail="Application not found or not accepted"
+ )
+
+ application = app_resp.data
+ creator_id = application["creator_id"]
+
+ # Check if proposal already exists
+ existing_proposal = supabase.table("proposals") \
+ .select("id") \
+ .eq("campaign_id", campaign_id) \
+ .eq("creator_id", creator_id) \
+ .eq("brand_id", brand_id) \
+ .execute()
+
+ if existing_proposal.data:
+ raise HTTPException(
+ status_code=400,
+ detail="A proposal already exists for this application"
+ )
+
+ # Create proposal from application
+ proposal_data = {
+ "campaign_id": campaign_id,
+ "creator_id": creator_id,
+ "brand_id": brand_id,
+ "subject": f"Proposal for {campaign_resp.data.get('title', 'Campaign')}",
+ "message": application.get("description", ""),
+ "proposed_amount": application.get("payment_max") or application.get("payment_min"),
+ "content_ideas": [],
+ "status": "pending"
+ }
+
+ proposal_response = supabase.table("proposals").insert(proposal_data).execute()
+
+ if not proposal_response.data:
+ raise HTTPException(status_code=500, detail="Failed to create proposal")
+
+ return {
+ "proposal": proposal_response.data[0],
+ "application_id": application_id,
+ "message": "Proposal created successfully. You can now negotiate with the creator."
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error creating proposal from application: {str(e)}"
+ ) from e
+
+
+@router.get("/creators/applications", response_model=List[CampaignApplicationResponse])
+async def get_creator_applications(
+ creator: dict = Depends(get_current_creator),
+ status: Optional[str] = Query(None, description="Filter by application status")
+):
+ """
+ Get all applications submitted by the current creator.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ query = supabase.table("campaign_applications") \
+ .select("*, campaigns!campaign_applications_campaign_id_fkey(id, title, brand_id, brands!campaigns_brand_id_fkey(company_name))") \
+ .eq("creator_id", creator_id) \
+ .order("created_at", desc=True)
+
+ if status:
+ query = query.eq("status", status)
+
+ response = query.execute()
+
+ applications = response.data or []
+
+ # Format response
+ formatted = []
+ for app in applications:
+ campaign_info = app.get("campaigns", {}) if isinstance(app.get("campaigns"), dict) else {}
+ brand_info = campaign_info.get("brands", {}) if isinstance(campaign_info.get("brands"), dict) else {}
+ formatted.append({
+ "id": app["id"],
+ "campaign_id": app["campaign_id"],
+ "creator_id": app["creator_id"],
+ "payment_min": app.get("payment_min"),
+ "payment_max": app.get("payment_max"),
+ "timeline_days": app.get("timeline_days"),
+ "timeline_weeks": app.get("timeline_weeks"),
+ "description": app.get("description"),
+ "message": app.get("message"),
+ "proposed_amount": app.get("proposed_amount"),
+ "status": app.get("status", "applied"),
+ "created_at": app["created_at"],
+ "updated_at": app["updated_at"],
+ "creator_name": None, # Not needed for creator's own applications
+ "creator_profile_picture": None,
+ "campaign_title": campaign_info.get("title") if campaign_info else None
+ })
+
+ return formatted
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching creator applications: {str(e)}"
+ ) from e
diff --git a/backend/app/api/routes/collaborations.py b/backend/app/api/routes/collaborations.py
new file mode 100644
index 0000000..425f178
--- /dev/null
+++ b/backend/app/api/routes/collaborations.py
@@ -0,0 +1,1431 @@
+"""
+Collaborations management routes for creator users.
+"""
+
+from fastapi import APIRouter, HTTPException, Depends, Query
+from pydantic import BaseModel, Field
+from typing import Optional, List
+from datetime import datetime, date
+import json
+from groq import Groq
+from app.core.supabase_clients import supabase_anon
+from app.core.dependencies import get_current_creator
+from app.core.config import settings
+
+router = APIRouter()
+
+
+class CollaborationResponse(BaseModel):
+ """Schema for collaboration response."""
+ id: str
+ creator1_id: str
+ creator2_id: str
+ collaboration_type: str
+ title: str
+ description: Optional[str]
+ status: str
+ match_score: Optional[float]
+ ai_suggestions: Optional[dict]
+ start_date: Optional[date]
+ end_date: Optional[date]
+ planned_deliverables: Optional[dict]
+ completed_deliverables: Optional[List[dict]]
+ initiator_id: Optional[str]
+ proposal_message: Optional[str]
+ response_message: Optional[str]
+ total_views: int
+ total_engagement: int
+ audience_growth: Optional[dict]
+ creator1_rating: Optional[int]
+ creator1_feedback: Optional[str]
+ creator2_rating: Optional[int]
+ creator2_feedback: Optional[str]
+ proposed_at: datetime
+ accepted_at: Optional[datetime]
+ completed_at: Optional[datetime]
+
+
+@router.get("/collaborations", response_model=List[CollaborationResponse])
+async def get_my_collaborations(
+ creator: dict = Depends(get_current_creator),
+ status: Optional[str] = Query(None, description="Filter by status"),
+ limit: int = Query(50, ge=1, le=100),
+ offset: int = Query(0, ge=0)
+):
+ """
+ Get all collaborations for the authenticated creator.
+
+ Returns collaborations where the creator is either creator1 or creator2.
+
+ - **status**: Optional filter by collaboration status (proposed, accepted, planning, active, completed, declined, cancelled)
+ - **limit**: Maximum number of results (default: 50, max: 100)
+ - **offset**: Number of results to skip for pagination
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Get collaborations where creator is creator1
+ query1 = supabase.table("creator_collaborations").select("*").eq("creator1_id", creator_id)
+
+ # Get collaborations where creator is creator2
+ query2 = supabase.table("creator_collaborations").select("*").eq("creator2_id", creator_id)
+
+ # Apply status filter if provided
+ if status:
+ query1 = query1.eq("status", status)
+ query2 = query2.eq("status", status)
+
+ # Execute both queries
+ response1 = query1.execute()
+ response2 = query2.execute()
+
+ # Combine results
+ all_collaborations = (response1.data or []) + (response2.data or [])
+
+ # Remove duplicates (in case of any edge cases)
+ seen = set()
+ unique_collaborations = []
+ for collab in all_collaborations:
+ if collab['id'] not in seen:
+ seen.add(collab['id'])
+ unique_collaborations.append(collab)
+
+ # Sort by proposed_at descending
+ unique_collaborations.sort(key=lambda x: x.get('proposed_at', ''), reverse=True)
+
+ # Apply pagination
+ paginated = unique_collaborations[offset:offset + limit]
+
+ return paginated
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching collaborations: {str(e)}"
+ ) from e
+
+
+@router.get("/collaborations/{collaboration_id}", response_model=CollaborationResponse)
+async def get_collaboration(
+ collaboration_id: str,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Get a single collaboration by ID.
+
+ Only returns the collaboration if the authenticated creator is involved (creator1 or creator2).
+
+ - **collaboration_id**: The collaboration ID
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Fetch collaboration
+ response = supabase.table("creator_collaborations") \
+ .select("*") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(
+ status_code=404,
+ detail="Collaboration not found"
+ )
+
+ collaboration = response.data
+
+ # Verify creator is involved (creator1 or creator2)
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(
+ status_code=403,
+ detail="You don't have access to this collaboration"
+ )
+
+ return collaboration
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ if "PGRST116" in str(e): # No rows returned
+ raise HTTPException(
+ status_code=404,
+ detail="Collaboration not found or you don't have access to it"
+ ) from e
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching collaboration: {str(e)}"
+ ) from e
+
+
+@router.get("/collaborations/stats/summary")
+async def get_collaborations_stats(
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Get collaboration statistics for the authenticated creator.
+
+ Returns counts of collaborations by status.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Get collaborations where creator is creator1
+ response1 = supabase.table("creator_collaborations") \
+ .select("status") \
+ .eq("creator1_id", creator_id) \
+ .execute()
+
+ # Get collaborations where creator is creator2
+ response2 = supabase.table("creator_collaborations") \
+ .select("status") \
+ .eq("creator2_id", creator_id) \
+ .execute()
+
+ # Combine results
+ collaborations = (response1.data or []) + (response2.data or [])
+
+ # Count by status
+ stats = {
+ "total": len(collaborations),
+ "proposed": 0,
+ "accepted": 0,
+ "planning": 0,
+ "active": 0,
+ "completed": 0,
+ "declined": 0,
+ "cancelled": 0
+ }
+
+ for collab in collaborations:
+ status = collab.get("status", "proposed")
+ if status in stats:
+ stats[status] += 1
+
+ return stats
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching collaboration stats: {str(e)}"
+ ) from e
+
+
+class CollaborationIdeasRequest(BaseModel):
+ """Request model for generating collaboration ideas."""
+ target_creator_id: str
+
+
+class CollaborationIdea(BaseModel):
+ """Schema for a single collaboration idea."""
+ title: str
+ description: str
+ collaboration_type: str
+ why_it_works: str
+
+
+class CollaborationIdeasResponse(BaseModel):
+ """Response model for collaboration ideas."""
+ ideas: List[CollaborationIdea]
+
+
+@router.post("/collaborations/generate-ideas", response_model=CollaborationIdeasResponse)
+async def generate_collaboration_ideas(
+ request: CollaborationIdeasRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Generate collaboration ideas between the current creator and a target creator using AI.
+
+ - **target_creator_id**: The ID of the creator to collaborate with
+ """
+ supabase = supabase_anon
+ current_creator_id = creator['id']
+ target_creator_id = request.target_creator_id
+
+ # Prevent self-collaboration
+ if current_creator_id == target_creator_id:
+ raise HTTPException(
+ status_code=400,
+ detail="Cannot generate collaboration ideas with yourself"
+ )
+
+ try:
+ # Fetch both creator profiles
+ current_creator_response = supabase.table("creators") \
+ .select("*") \
+ .eq("id", current_creator_id) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ target_creator_response = supabase.table("creators") \
+ .select("*") \
+ .eq("id", target_creator_id) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ if not current_creator_response.data:
+ raise HTTPException(status_code=404, detail="Current creator profile not found")
+ if not target_creator_response.data:
+ raise HTTPException(status_code=404, detail="Target creator profile not found")
+
+ current_creator = current_creator_response.data
+ target_creator = target_creator_response.data
+
+ # Build prompt for Groq AI
+ prompt = f"""You are an expert at matching content creators for collaborations. Analyze the following two creator profiles and suggest 5 creative, specific collaboration ideas that would work well between them.
+
+Creator 1 Profile:
+- Name: {current_creator.get('display_name', 'Unknown')}
+- Tagline: {current_creator.get('tagline', 'N/A')}
+- Bio: {current_creator.get('bio', 'N/A')}
+- Primary Niche: {current_creator.get('primary_niche', 'N/A')}
+- Secondary Niches: {', '.join(current_creator.get('secondary_niches', []) or [])}
+- Content Types: {', '.join(current_creator.get('content_types', []) or [])}
+- Collaboration Types Open To: {', '.join(current_creator.get('collaboration_types', []) or [])}
+- Total Followers: {current_creator.get('total_followers', 0)}
+- Engagement Rate: {current_creator.get('engagement_rate', 0)}%
+
+Creator 2 Profile:
+- Name: {target_creator.get('display_name', 'Unknown')}
+- Tagline: {target_creator.get('tagline', 'N/A')}
+- Bio: {target_creator.get('bio', 'N/A')}
+- Primary Niche: {target_creator.get('primary_niche', 'N/A')}
+- Secondary Niches: {', '.join(target_creator.get('secondary_niches', []) or [])}
+- Content Types: {', '.join(target_creator.get('content_types', []) or [])}
+- Collaboration Types Open To: {', '.join(target_creator.get('collaboration_types', []) or [])}
+- Total Followers: {target_creator.get('total_followers', 0)}
+- Engagement Rate: {target_creator.get('engagement_rate', 0)}%
+
+Please provide 5 collaboration ideas. For each idea, provide:
+1. A catchy title (max 60 characters)
+2. A detailed description (2-3 sentences explaining the collaboration)
+3. The collaboration type (e.g., "Video Collaboration", "Cross-Promotion", "Joint Series", "Challenge", "Podcast", etc.)
+4. Why it works (1-2 sentences explaining why these creators are a good match for this idea)
+
+Format your response as a JSON array with this exact structure:
+[
+ {{
+ "title": "Idea Title",
+ "description": "Detailed description of the collaboration idea",
+ "collaboration_type": "Type of collaboration",
+ "why_it_works": "Explanation of why this works for these creators"
+ }},
+ ...
+]
+
+Return ONLY the JSON array, no additional text or markdown formatting."""
+
+ # Call Groq API using official SDK
+ api_key = settings.groq_api_key
+ if not api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=api_key)
+
+ try:
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert strategist who crafts detailed, actionable collaboration ideas for content creators. Always respond with valid JSON only.",
+ },
+ {"role": "user", "content": prompt},
+ ],
+ temperature=0.8,
+ max_completion_tokens=1200,
+ top_p=1,
+ stream=False,
+ )
+
+ content = completion.choices[0].message.content if completion.choices else ""
+
+ # Parse JSON from the response
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ ideas_data = json.loads(content)
+
+ # Validate and convert to our model
+ ideas = []
+ for idea in ideas_data[:5]: # Take up to 5 ideas
+ ideas.append(CollaborationIdea(
+ title=idea.get("title", "Untitled Collaboration"),
+ description=idea.get("description", ""),
+ collaboration_type=idea.get("collaboration_type", "General Collaboration"),
+ why_it_works=idea.get("why_it_works", "")
+ ))
+
+ if not ideas:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to generate collaboration ideas. Please try again."
+ )
+
+ return CollaborationIdeasResponse(ideas=ideas)
+
+ except json.JSONDecodeError as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to parse AI response: {str(e)}"
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=502,
+ detail=f"GROQ API error: {str(e)}"
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error generating collaboration ideas: {str(e)}"
+ ) from e
+
+
+class RecommendCreatorRequest(BaseModel):
+ """Request model for recommending best creator for a collaboration idea."""
+ collaboration_idea: str
+ candidate_creator_ids: List[str]
+
+
+class CreatorRecommendation(BaseModel):
+ """Schema for a creator recommendation."""
+ creator_id: str
+ display_name: str
+ profile_picture_url: Optional[str]
+ primary_niche: str
+ match_score: float
+ reasoning: str
+
+
+class RecommendCreatorResponse(BaseModel):
+ """Response model for creator recommendation."""
+ recommended_creator: CreatorRecommendation
+ alternatives: List[CreatorRecommendation]
+
+
+@router.post("/collaborations/recommend-creator", response_model=RecommendCreatorResponse)
+async def recommend_creator_for_idea(
+ request: RecommendCreatorRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Recommend the best creator from a list of candidates for a specific collaboration idea.
+
+ - **collaboration_idea**: Description of the collaboration idea/content
+ - **candidate_creator_ids**: List of creator IDs to choose from
+ """
+ supabase = supabase_anon
+ current_creator_id = creator['id']
+
+ if not request.candidate_creator_ids:
+ raise HTTPException(
+ status_code=400,
+ detail="At least one candidate creator ID is required"
+ )
+
+ try:
+ # Fetch current creator profile
+ current_creator_response = supabase.table("creators") \
+ .select("*") \
+ .eq("id", current_creator_id) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ if not current_creator_response.data:
+ raise HTTPException(status_code=404, detail="Current creator profile not found")
+
+ current_creator = current_creator_response.data
+
+ # Fetch all candidate creator profiles
+ candidate_creators = []
+ for candidate_id in request.candidate_creator_ids:
+ if candidate_id == current_creator_id:
+ continue # Skip self
+ response = supabase.table("creators") \
+ .select("*") \
+ .eq("id", candidate_id) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+ if response.data:
+ candidate_creators.append(response.data)
+
+ if not candidate_creators:
+ raise HTTPException(
+ status_code=404,
+ detail="No valid candidate creators found"
+ )
+
+ # Build prompt for Groq AI
+ candidates_info = []
+ for idx, cand in enumerate(candidate_creators):
+ candidates_info.append(f"""
+Candidate {idx + 1} (ID: {cand.get('id', 'unknown')}):
+- Name: {cand.get('display_name', 'Unknown')}
+- Tagline: {cand.get('tagline', 'N/A')}
+- Bio: {cand.get('bio', 'N/A')}
+- Primary Niche: {cand.get('primary_niche', 'N/A')}
+- Secondary Niches: {', '.join(cand.get('secondary_niches', []) or [])}
+- Content Types: {', '.join(cand.get('content_types', []) or [])}
+- Collaboration Types Open To: {', '.join(cand.get('collaboration_types', []) or [])}
+- Total Followers: {cand.get('total_followers', 0)}
+- Engagement Rate: {cand.get('engagement_rate', 0)}%
+- Years of Experience: {cand.get('years_of_experience', 'N/A')}
+""")
+
+ prompt = f"""You are an expert at matching content creators for collaborations. A creator wants to collaborate on the following idea:
+
+COLLABORATION IDEA:
+{request.collaboration_idea}
+
+CURRENT CREATOR PROFILE:
+- Name: {current_creator.get('display_name', 'Unknown')}
+- Tagline: {current_creator.get('tagline', 'N/A')}
+- Bio: {current_creator.get('bio', 'N/A')}
+- Primary Niche: {current_creator.get('primary_niche', 'N/A')}
+- Secondary Niches: {', '.join(current_creator.get('secondary_niches', []) or [])}
+- Content Types: {', '.join(current_creator.get('content_types', []) or [])}
+- Collaboration Types Open To: {', '.join(current_creator.get('collaboration_types', []) or [])}
+
+CANDIDATE CREATORS:
+{''.join(candidates_info)}
+
+Analyze which candidate creator would be the BEST match for this collaboration idea. Consider:
+1. Niche compatibility
+2. Content type alignment
+3. Audience synergy
+4. Collaboration type preferences
+5. How well the idea fits each candidate's style and strengths
+
+Rank all candidates from best to worst match. For each candidate, provide:
+- A match score (0-100)
+- Detailed reasoning explaining why they are or aren't a good fit
+
+Format your response as JSON with this exact structure:
+{{
+ "recommendations": [
+ {{
+ "creator_id": "candidate_id_here",
+ "match_score": 85,
+ "reasoning": "Detailed explanation of why this creator is a good/bad match for the idea"
+ }},
+ ...
+ ]
+}}
+
+Return ONLY the JSON object, no additional text or markdown formatting."""
+
+ # Call Groq API
+ api_key = settings.groq_api_key
+ if not api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ from groq import Groq
+ groq_client = Groq(api_key=api_key)
+
+ try:
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert strategist who analyzes creator collaborations. Always respond with valid JSON only.",
+ },
+ {"role": "user", "content": prompt},
+ ],
+ temperature=0.7,
+ max_completion_tokens=1500,
+ top_p=1,
+ stream=False,
+ )
+
+ content = completion.choices[0].message.content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result_data = json.loads(content)
+ recommendations = result_data.get("recommendations", [])
+
+ if not recommendations:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to get recommendations from AI"
+ )
+
+ # Map recommendations to creator data
+ creator_map = {cand['id']: cand for cand in candidate_creators}
+ ranked_creators = []
+
+ for rec in recommendations:
+ creator_id = rec.get("creator_id")
+ if creator_id in creator_map:
+ creator_data = creator_map[creator_id]
+ ranked_creators.append(CreatorRecommendation(
+ creator_id=creator_id,
+ display_name=creator_data.get('display_name', 'Unknown'),
+ profile_picture_url=creator_data.get('profile_picture_url'),
+ primary_niche=creator_data.get('primary_niche', ''),
+ match_score=float(rec.get("match_score", 0)),
+ reasoning=rec.get("reasoning", "")
+ ))
+
+ if not ranked_creators:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to process recommendations"
+ )
+
+ # Return best match and alternatives
+ recommended = ranked_creators[0]
+ alternatives = ranked_creators[1:] if len(ranked_creators) > 1 else []
+
+ return RecommendCreatorResponse(
+ recommended_creator=recommended,
+ alternatives=alternatives
+ )
+
+ except json.JSONDecodeError as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to parse AI response: {str(e)}"
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error getting recommendation: {str(e)}"
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error recommending creator: {str(e)}"
+ ) from e
+
+
+# ========== Collaboration Proposal & Management ==========
+
+class ProposeCollaborationRequest(BaseModel):
+ """Request model for proposing a collaboration."""
+ target_creator_id: str
+ collaboration_type: str
+ title: str
+ description: Optional[str] = None
+ proposal_message: Optional[str] = None
+ start_date: Optional[date] = None
+ end_date: Optional[date] = None
+ planned_deliverables: Optional[dict] = None
+
+
+@router.post("/collaborations/propose", response_model=CollaborationResponse)
+async def propose_collaboration(
+ request: ProposeCollaborationRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Propose a collaboration to another creator.
+ """
+ supabase = supabase_anon
+ current_creator_id = creator['id']
+ target_creator_id = request.target_creator_id
+
+ # Prevent self-collaboration
+ if current_creator_id == target_creator_id:
+ raise HTTPException(
+ status_code=400,
+ detail="Cannot propose collaboration to yourself"
+ )
+
+ try:
+ # Verify target creator exists
+ target_response = supabase.table("creators") \
+ .select("id") \
+ .eq("id", target_creator_id) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ if not target_response.data:
+ raise HTTPException(status_code=404, detail="Target creator not found")
+
+ # Determine creator1 and creator2 (always use lower UUID first for consistency)
+ creator_ids = sorted([current_creator_id, target_creator_id])
+ creator1_id = creator_ids[0]
+ creator2_id = creator_ids[1]
+
+ # Check if collaboration already exists
+ existing = supabase.table("creator_collaborations") \
+ .select("id") \
+ .eq("creator1_id", creator1_id) \
+ .eq("creator2_id", creator2_id) \
+ .eq("title", request.title) \
+ .execute()
+
+ if existing.data:
+ raise HTTPException(
+ status_code=400,
+ detail="A collaboration with this title already exists between you and this creator"
+ )
+
+ # Create collaboration
+ collaboration_data = {
+ "creator1_id": creator1_id,
+ "creator2_id": creator2_id,
+ "collaboration_type": request.collaboration_type,
+ "title": request.title,
+ "description": request.description,
+ "status": "proposed",
+ "initiator_id": current_creator_id,
+ "proposal_message": request.proposal_message,
+ "start_date": request.start_date.isoformat() if request.start_date else None,
+ "end_date": request.end_date.isoformat() if request.end_date else None,
+ "planned_deliverables": request.planned_deliverables or {}
+ }
+
+ response = supabase.table("creator_collaborations") \
+ .insert(collaboration_data) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to create collaboration proposal")
+
+ return response.data[0]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error proposing collaboration: {str(e)}"
+ ) from e
+
+
+class AcceptDeclineRequest(BaseModel):
+ """Request model for accepting or declining a collaboration."""
+ response_message: Optional[str] = None
+
+
+@router.post("/collaborations/{collaboration_id}/accept", response_model=CollaborationResponse)
+async def accept_collaboration(
+ collaboration_id: str,
+ request: AcceptDeclineRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Accept a collaboration proposal.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Fetch collaboration
+ collab_response = supabase.table("creator_collaborations") \
+ .select("*") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not collab_response.data:
+ raise HTTPException(status_code=404, detail="Collaboration not found")
+
+ collaboration = collab_response.data
+
+ # Verify creator is the recipient (not the initiator)
+ if collaboration.get("initiator_id") == creator_id:
+ raise HTTPException(
+ status_code=400,
+ detail="You cannot accept your own proposal"
+ )
+
+ # Verify creator is involved
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(
+ status_code=403,
+ detail="You don't have access to this collaboration"
+ )
+
+ # Verify status is 'proposed'
+ if collaboration.get("status") != "proposed":
+ raise HTTPException(
+ status_code=400,
+ detail=f"Cannot accept collaboration with status: {collaboration.get('status')}"
+ )
+
+ # Update collaboration
+ update_data = {
+ "status": "accepted",
+ "accepted_at": datetime.now().isoformat(),
+ "response_message": request.response_message
+ }
+
+ response = supabase.table("creator_collaborations") \
+ .update(update_data) \
+ .eq("id", collaboration_id) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to accept collaboration")
+
+ return response.data[0]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error accepting collaboration: {str(e)}"
+ ) from e
+
+
+@router.post("/collaborations/{collaboration_id}/decline", response_model=CollaborationResponse)
+async def decline_collaboration(
+ collaboration_id: str,
+ request: AcceptDeclineRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Decline a collaboration proposal.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Fetch collaboration
+ collab_response = supabase.table("creator_collaborations") \
+ .select("*") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not collab_response.data:
+ raise HTTPException(status_code=404, detail="Collaboration not found")
+
+ collaboration = collab_response.data
+
+ # Verify creator is the recipient (not the initiator)
+ if collaboration.get("initiator_id") == creator_id:
+ raise HTTPException(
+ status_code=400,
+ detail="You cannot decline your own proposal"
+ )
+
+ # Verify creator is involved
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(
+ status_code=403,
+ detail="You don't have access to this collaboration"
+ )
+
+ # Verify status is 'proposed'
+ if collaboration.get("status") != "proposed":
+ raise HTTPException(
+ status_code=400,
+ detail=f"Cannot decline collaboration with status: {collaboration.get('status')}"
+ )
+
+ # Update collaboration
+ update_data = {
+ "status": "declined",
+ "response_message": request.response_message
+ }
+
+ response = supabase.table("creator_collaborations") \
+ .update(update_data) \
+ .eq("id", collaboration_id) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to decline collaboration")
+
+ return response.data[0]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error declining collaboration: {str(e)}"
+ ) from e
+
+
+# ========== Collaboration Workspace ==========
+
+class CollaborationWorkspaceResponse(BaseModel):
+ """Response model for collaboration workspace."""
+ collaboration: CollaborationResponse
+ deliverables: List[dict]
+ messages: List[dict]
+ assets: List[dict]
+ feedback: List[dict]
+ other_creator: dict
+
+
+@router.get("/collaborations/{collaboration_id}/workspace", response_model=CollaborationWorkspaceResponse)
+async def get_collaboration_workspace(
+ collaboration_id: str,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Get full collaboration workspace including deliverables, messages, and assets.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Fetch collaboration
+ collab_response = supabase.table("creator_collaborations") \
+ .select("*") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not collab_response.data:
+ raise HTTPException(status_code=404, detail="Collaboration not found")
+
+ collaboration = collab_response.data
+
+ # Verify creator is involved
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(
+ status_code=403,
+ detail="You don't have access to this collaboration"
+ )
+
+ # Get other creator info
+ other_creator_id = collaboration.get("creator2_id") if collaboration.get("creator1_id") == creator_id else collaboration.get("creator1_id")
+ other_creator_response = supabase.table("creators") \
+ .select("id, display_name, profile_picture_url, primary_niche") \
+ .eq("id", other_creator_id) \
+ .single() \
+ .execute()
+
+ other_creator = other_creator_response.data if other_creator_response.data else {}
+
+ # Get deliverables
+ deliverables_response = supabase.table("collaboration_deliverables") \
+ .select("*") \
+ .eq("collaboration_id", collaboration_id) \
+ .order("created_at", desc=False) \
+ .execute()
+
+ deliverables = deliverables_response.data or []
+
+ # Get messages
+ messages_response = supabase.table("collaboration_messages") \
+ .select("*") \
+ .eq("collaboration_id", collaboration_id) \
+ .order("created_at", desc=False) \
+ .execute()
+
+ messages = messages_response.data or []
+
+ # Get assets
+ assets_response = supabase.table("collaboration_assets") \
+ .select("*") \
+ .eq("collaboration_id", collaboration_id) \
+ .order("created_at", desc=True) \
+ .execute()
+
+ assets = assets_response.data or []
+
+ # Get feedback
+ feedback_response = supabase.table("collaboration_feedback") \
+ .select("*") \
+ .eq("collaboration_id", collaboration_id) \
+ .execute()
+
+ feedback = feedback_response.data or []
+
+ return CollaborationWorkspaceResponse(
+ collaboration=collaboration,
+ deliverables=deliverables,
+ messages=messages,
+ assets=assets,
+ feedback=feedback,
+ other_creator=other_creator
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching workspace: {str(e)}"
+ ) from e
+
+
+# ========== Deliverables Management ==========
+
+class CreateDeliverableRequest(BaseModel):
+ """Request model for creating a deliverable."""
+ description: str
+ due_date: Optional[date] = None
+
+
+@router.post("/collaborations/{collaboration_id}/deliverables", response_model=dict)
+async def create_deliverable(
+ collaboration_id: str,
+ request: CreateDeliverableRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Create a new deliverable for a collaboration.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Verify collaboration access
+ collab_response = supabase.table("creator_collaborations") \
+ .select("creator1_id, creator2_id, status") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not collab_response.data:
+ raise HTTPException(status_code=404, detail="Collaboration not found")
+
+ collaboration = collab_response.data
+
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(status_code=403, detail="You don't have access to this collaboration")
+
+ if collaboration.get("status") not in ["accepted", "planning", "active"]:
+ raise HTTPException(
+ status_code=400,
+ detail="Can only add deliverables to accepted, planning, or active collaborations"
+ )
+
+ # Create deliverable
+ deliverable_data = {
+ "collaboration_id": collaboration_id,
+ "description": request.description,
+ "due_date": request.due_date.isoformat() if request.due_date else None,
+ "status": "pending"
+ }
+
+ response = supabase.table("collaboration_deliverables") \
+ .insert(deliverable_data) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to create deliverable")
+
+ return response.data[0]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error creating deliverable: {str(e)}"
+ ) from e
+
+
+class UpdateDeliverableRequest(BaseModel):
+ """Request model for updating a deliverable."""
+ description: Optional[str] = None
+ due_date: Optional[date] = None
+ status: Optional[str] = None
+ submission_url: Optional[str] = None
+
+
+@router.patch("/collaborations/{collaboration_id}/deliverables/{deliverable_id}", response_model=dict)
+async def update_deliverable(
+ collaboration_id: str,
+ deliverable_id: str,
+ request: UpdateDeliverableRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Update a deliverable.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Verify collaboration access
+ collab_response = supabase.table("creator_collaborations") \
+ .select("creator1_id, creator2_id") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not collab_response.data:
+ raise HTTPException(status_code=404, detail="Collaboration not found")
+
+ collaboration = collab_response.data
+
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(status_code=403, detail="You don't have access to this collaboration")
+
+ # Verify deliverable exists and belongs to collaboration
+ deliverable_response = supabase.table("collaboration_deliverables") \
+ .select("*") \
+ .eq("id", deliverable_id) \
+ .eq("collaboration_id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not deliverable_response.data:
+ raise HTTPException(status_code=404, detail="Deliverable not found")
+
+ # Build update data
+ update_data = {}
+ if request.description is not None:
+ update_data["description"] = request.description
+ if request.due_date is not None:
+ update_data["due_date"] = request.due_date.isoformat()
+ if request.status is not None:
+ update_data["status"] = request.status
+ if request.submission_url is not None:
+ update_data["submission_url"] = request.submission_url
+
+ response = supabase.table("collaboration_deliverables") \
+ .update(update_data) \
+ .eq("id", deliverable_id) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to update deliverable")
+
+ return response.data[0]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error updating deliverable: {str(e)}"
+ ) from e
+
+
+# ========== Messages ==========
+
+class SendMessageRequest(BaseModel):
+ """Request model for sending a message."""
+ message: str
+
+
+@router.post("/collaborations/{collaboration_id}/messages", response_model=dict)
+async def send_message(
+ collaboration_id: str,
+ request: SendMessageRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Send a message in a collaboration.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Verify collaboration access
+ collab_response = supabase.table("creator_collaborations") \
+ .select("creator1_id, creator2_id, status") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not collab_response.data:
+ raise HTTPException(status_code=404, detail="Collaboration not found")
+
+ collaboration = collab_response.data
+
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(status_code=403, detail="You don't have access to this collaboration")
+
+ if collaboration.get("status") in ["declined", "cancelled"]:
+ raise HTTPException(
+ status_code=400,
+ detail="Cannot send messages to declined or cancelled collaborations"
+ )
+
+ # Create message
+ message_data = {
+ "collaboration_id": collaboration_id,
+ "sender_id": creator_id,
+ "message": request.message
+ }
+
+ response = supabase.table("collaboration_messages") \
+ .insert(message_data) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to send message")
+
+ return response.data[0]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error sending message: {str(e)}"
+ ) from e
+
+
+# ========== Assets ==========
+
+class UploadAssetRequest(BaseModel):
+ """Request model for uploading an asset."""
+ url: str
+ type: Optional[str] = None
+
+
+@router.post("/collaborations/{collaboration_id}/assets", response_model=dict)
+async def upload_asset(
+ collaboration_id: str,
+ request: UploadAssetRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Upload/share an asset (file URL) in a collaboration.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Verify collaboration access
+ collab_response = supabase.table("creator_collaborations") \
+ .select("creator1_id, creator2_id, status") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not collab_response.data:
+ raise HTTPException(status_code=404, detail="Collaboration not found")
+
+ collaboration = collab_response.data
+
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(status_code=403, detail="You don't have access to this collaboration")
+
+ if collaboration.get("status") in ["declined", "cancelled"]:
+ raise HTTPException(
+ status_code=400,
+ detail="Cannot upload assets to declined or cancelled collaborations"
+ )
+
+ # Create asset
+ asset_data = {
+ "collaboration_id": collaboration_id,
+ "uploaded_by": creator_id,
+ "url": request.url,
+ "type": request.type
+ }
+
+ response = supabase.table("collaboration_assets") \
+ .insert(asset_data) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to upload asset")
+
+ return response.data[0]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error uploading asset: {str(e)}"
+ ) from e
+
+
+# ========== Completion & Feedback ==========
+
+class CompleteCollaborationRequest(BaseModel):
+ """Request model for completing a collaboration."""
+ pass # No additional fields needed
+
+
+@router.post("/collaborations/{collaboration_id}/complete", response_model=CollaborationResponse)
+async def complete_collaboration(
+ collaboration_id: str,
+ request: CompleteCollaborationRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Mark a collaboration as complete. Both creators must mark it complete.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Fetch collaboration
+ collab_response = supabase.table("creator_collaborations") \
+ .select("*") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not collab_response.data:
+ raise HTTPException(status_code=404, detail="Collaboration not found")
+
+ collaboration = collab_response.data
+
+ # Verify creator is involved
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(status_code=403, detail="You don't have access to this collaboration")
+
+ # Verify status allows completion
+ if collaboration.get("status") not in ["accepted", "planning", "active"]:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Cannot complete collaboration with status: {collaboration.get('status')}"
+ )
+
+ # Check if all deliverables are completed
+ deliverables_response = supabase.table("collaboration_deliverables") \
+ .select("id, status") \
+ .eq("collaboration_id", collaboration_id) \
+ .execute()
+
+ deliverables = deliverables_response.data or []
+ if deliverables:
+ incomplete = [d for d in deliverables if d.get("status") != "completed"]
+ if incomplete:
+ raise HTTPException(
+ status_code=400,
+ detail="All deliverables must be completed before marking collaboration as complete"
+ )
+
+ # Update collaboration status
+ update_data = {
+ "status": "completed",
+ "completed_at": datetime.now().isoformat()
+ }
+
+ response = supabase.table("creator_collaborations") \
+ .update(update_data) \
+ .eq("id", collaboration_id) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to complete collaboration")
+
+ return response.data[0]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error completing collaboration: {str(e)}"
+ ) from e
+
+
+class SubmitFeedbackRequest(BaseModel):
+ """Request model for submitting feedback."""
+ rating: int = Field(..., ge=1, le=5)
+ feedback: Optional[str] = None
+
+
+@router.post("/collaborations/{collaboration_id}/feedback", response_model=dict)
+async def submit_feedback(
+ collaboration_id: str,
+ request: SubmitFeedbackRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Submit feedback and rating for a completed collaboration.
+ """
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ # Fetch collaboration
+ collab_response = supabase.table("creator_collaborations") \
+ .select("*") \
+ .eq("id", collaboration_id) \
+ .single() \
+ .execute()
+
+ if not collab_response.data:
+ raise HTTPException(status_code=404, detail="Collaboration not found")
+
+ collaboration = collab_response.data
+
+ # Verify creator is involved
+ if collaboration.get("creator1_id") != creator_id and collaboration.get("creator2_id") != creator_id:
+ raise HTTPException(status_code=403, detail="You don't have access to this collaboration")
+
+ # Verify collaboration is completed
+ if collaboration.get("status") != "completed":
+ raise HTTPException(
+ status_code=400,
+ detail="Can only submit feedback for completed collaborations"
+ )
+
+ # Determine the other creator (the one receiving feedback)
+ other_creator_id = collaboration.get("creator2_id") if collaboration.get("creator1_id") == creator_id else collaboration.get("creator1_id")
+
+ # Check if feedback already exists
+ existing_feedback = supabase.table("collaboration_feedback") \
+ .select("id") \
+ .eq("collaboration_id", collaboration_id) \
+ .eq("from_creator_id", creator_id) \
+ .eq("to_creator_id", other_creator_id) \
+ .execute()
+
+ feedback_data = {
+ "collaboration_id": collaboration_id,
+ "from_creator_id": creator_id,
+ "to_creator_id": other_creator_id,
+ "rating": request.rating,
+ "feedback": request.feedback
+ }
+
+ if existing_feedback.data and len(existing_feedback.data) > 0:
+ # Update existing feedback
+ response = supabase.table("collaboration_feedback") \
+ .update(feedback_data) \
+ .eq("id", existing_feedback.data[0]["id"]) \
+ .execute()
+ else:
+ # Create new feedback
+ response = supabase.table("collaboration_feedback") \
+ .insert(feedback_data) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(status_code=500, detail="Failed to submit feedback")
+
+ return response.data[0] if isinstance(response.data, list) else response.data
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error submitting feedback: {str(e)}"
+ ) from e
+
diff --git a/backend/app/api/routes/creators.py b/backend/app/api/routes/creators.py
new file mode 100644
index 0000000..55a4d10
--- /dev/null
+++ b/backend/app/api/routes/creators.py
@@ -0,0 +1,507 @@
+"""
+Creators listing routes for browsing all creators.
+"""
+
+from fastapi import APIRouter, HTTPException, Depends, Query
+from pydantic import BaseModel
+from typing import Optional, List, Tuple
+from math import exp
+from datetime import datetime
+import json
+from groq import Groq
+from app.core.config import settings
+from app.core.supabase_clients import supabase_anon
+from app.core.dependencies import get_current_creator
+router = APIRouter()
+
+
+class CreatorBasicResponse(BaseModel):
+ """Basic creator info for card display."""
+ id: str
+ display_name: str
+ tagline: Optional[str]
+ bio: Optional[str]
+ profile_picture_url: Optional[str]
+ primary_niche: str
+ secondary_niches: Optional[List[str]]
+ total_followers: int
+ engagement_rate: Optional[float]
+ is_verified_creator: bool
+ profile_completion_percentage: int
+
+
+class CreatorFullResponse(BaseModel):
+ """Full creator details for expanded view."""
+ id: str
+ user_id: str
+ display_name: str
+ tagline: Optional[str]
+ bio: Optional[str]
+ profile_picture_url: Optional[str]
+ cover_image_url: Optional[str]
+ website_url: Optional[str]
+ youtube_url: Optional[str]
+ youtube_handle: Optional[str]
+ youtube_subscribers: Optional[int]
+ instagram_url: Optional[str]
+ instagram_handle: Optional[str]
+ instagram_followers: Optional[int]
+ tiktok_url: Optional[str]
+ tiktok_handle: Optional[str]
+ tiktok_followers: Optional[int]
+ twitter_url: Optional[str]
+ twitter_handle: Optional[str]
+ twitter_followers: Optional[int]
+ twitch_url: Optional[str]
+ twitch_handle: Optional[str]
+ twitch_followers: Optional[int]
+ linkedin_url: Optional[str]
+ facebook_url: Optional[str]
+ primary_niche: str
+ secondary_niches: Optional[List[str]]
+ content_types: Optional[List[str]]
+ content_language: Optional[List[str]]
+ total_followers: int
+ total_reach: Optional[int]
+ average_views: Optional[int]
+ engagement_rate: Optional[float]
+ audience_age_primary: Optional[str]
+ audience_gender_split: Optional[dict]
+ audience_locations: Optional[dict]
+ audience_interests: Optional[List[str]]
+ average_engagement_per_post: Optional[int]
+ posting_frequency: Optional[str]
+ best_performing_content_type: Optional[str]
+ years_of_experience: Optional[int]
+ content_creation_full_time: bool
+ team_size: int
+ equipment_quality: Optional[str]
+ editing_software: Optional[List[str]]
+ collaboration_types: Optional[List[str]]
+ preferred_brands_style: Optional[List[str]]
+ rate_per_post: Optional[float]
+ rate_per_video: Optional[float]
+ rate_per_story: Optional[float]
+ rate_per_reel: Optional[float]
+ rate_negotiable: bool
+ accepts_product_only_deals: bool
+ minimum_deal_value: Optional[float]
+ preferred_payment_terms: Optional[str]
+ portfolio_links: Optional[List[str]]
+ past_brand_collaborations: Optional[List[str]]
+ case_study_links: Optional[List[str]]
+ media_kit_url: Optional[str]
+ is_verified_creator: bool
+ profile_completion_percentage: int
+ created_at: Optional[str]
+ last_active_at: Optional[str]
+
+
+@router.get("/creators", response_model=List[CreatorBasicResponse])
+async def list_creators(
+ creator: dict = Depends(get_current_creator),
+ search: Optional[str] = Query(None, description="Search by name, niche, or bio"),
+ niche: Optional[str] = Query(None, description="Filter by primary niche"),
+ limit: int = Query(50, ge=1, le=100),
+ offset: int = Query(0, ge=0)
+):
+ """
+ List all creators (excluding the current authenticated creator).
+
+ - **search**: Search in display_name, tagline, bio, primary_niche, secondary_niches
+ - **niche**: Filter by primary niche
+ - **limit**: Maximum number of results (default: 50, max: 100)
+ - **offset**: Number of results to skip for pagination
+ """
+ supabase = supabase_anon
+ current_creator_id = creator['id']
+
+ try:
+ # Build query - exclude current creator and only show active creators
+ query = supabase.table("creators").select("*").eq("is_active", True).neq("id", current_creator_id)
+
+ # Apply niche filter if provided
+ if niche:
+ query = query.eq("primary_niche", niche)
+
+ # Apply pagination and ordering
+ # Note: We fetch more results if search is provided, then filter in Python
+ fetch_limit = (limit * 3) if search else limit # Fetch more for search filtering
+ query = query.order("total_followers", desc=True).range(offset, offset + fetch_limit - 1)
+
+ response = query.execute()
+
+ creators = response.data if response.data else []
+
+ # Apply search filtering if provided
+ if search:
+ search_term = search.lower()
+ filtered_creators = []
+ for c in creators:
+ # Check if search term matches any field
+ matches = (
+ (c.get("display_name", "").lower().find(search_term) >= 0) or
+ (c.get("tagline", "").lower().find(search_term) >= 0 if c.get("tagline") else False) or
+ (c.get("bio", "").lower().find(search_term) >= 0 if c.get("bio") else False) or
+ (c.get("primary_niche", "").lower().find(search_term) >= 0) or
+ any(search_term in (niche or "").lower() for niche in (c.get("secondary_niches") or []))
+ )
+ if matches:
+ filtered_creators.append(c)
+ # Apply limit after filtering
+ creators = filtered_creators[:limit]
+
+ return creators
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching creators: {str(e)}"
+ ) from e
+
+
+class CreatorRecommendation(BaseModel):
+ id: str
+ display_name: str
+ profile_picture_url: Optional[str]
+ primary_niche: Optional[str]
+ total_followers: Optional[int]
+ engagement_rate: Optional[float]
+ top_platforms: Optional[List[str]] = None
+ match_score: float
+ reason: str
+
+
+@router.get("/creators/recommendations", response_model=List[CreatorRecommendation])
+async def get_creator_recommendations(
+ creator: dict = Depends(get_current_creator),
+ limit: int = Query(4, ge=1, le=10),
+ use_ai: bool = Query(True, description="Use Groq to rerank top candidates")
+):
+ """
+ Recommend top creators to collaborate with the current creator.
+ Combines rules-based scoring with optional Groq reranking for reasons and fine-tuning.
+ """
+ supabase = supabase_anon
+ current_creator_id = creator["id"]
+
+ try:
+ # Fetch current creator full profile
+ current_resp = supabase.table("creators") \
+ .select("*") \
+ .eq("id", current_creator_id) \
+ .single() \
+ .execute()
+ if not current_resp.data:
+ raise HTTPException(status_code=404, detail="Current creator not found")
+ me = current_resp.data
+
+ # Fetch candidate creators (active, not self)
+ candidates_resp = supabase.table("creators") \
+ .select("*") \
+ .eq("is_active", True) \
+ .neq("id", current_creator_id) \
+ .order("total_followers", desc=True) \
+ .limit(200) \
+ .execute()
+ candidates = candidates_resp.data or []
+
+ if not candidates:
+ return []
+
+ # Utility helpers
+ def list_overlap(a: Optional[List[str]], b: Optional[List[str]]) -> int:
+ sa = set((a or []))
+ sb = set((b or []))
+ return len(sa.intersection(sb))
+
+ def followers_proximity(a: Optional[int], b: Optional[int]) -> float:
+ if not a or not b or a <= 0 or b <= 0:
+ return 0.5
+ ratio = max(a, b) / max(1, min(a, b))
+ # Sigmoid-like decay with ratio; closer to 1 → closer to 1.0 score
+ return max(0.0, min(1.0, 1 / (1 + (ratio - 1))))
+
+ def normalize_percent(x: Optional[float]) -> float:
+ if x is None:
+ return 0.0
+ return max(0.0, min(1.0, x / 10.0)) # treat 10% as "good" baseline
+
+ def recency_score(last_active_at: Optional[str]) -> float:
+ if not last_active_at:
+ return 0.5
+ try:
+ dt = datetime.fromisoformat(last_active_at.replace("Z", "+00:00"))
+ days = max(0.0, (datetime.now(dt.tzinfo) - dt).days)
+ # Decay after 30 days
+ return max(0.0, min(1.0, 1 / (1 + days / 30.0)))
+ except Exception:
+ return 0.5
+
+ me_niche = me.get("primary_niche")
+ me_secondary = me.get("secondary_niches") or []
+ me_types = me.get("content_types") or []
+ me_collab = me.get("collaboration_types") or []
+ me_langs = me.get("content_language") or []
+ me_followers = me.get("total_followers") or 0
+
+ # Score candidates
+ scored: List[Tuple[dict, float, str]] = []
+ for c in candidates:
+ reason_bits = []
+ score = 0.0
+
+ # Niche similarity (25)
+ niche_pts = 0.0
+ if c.get("primary_niche") == me_niche:
+ niche_pts += 0.7
+ reason_bits.append("same primary niche")
+ sec_overlap = list_overlap(me_secondary, c.get("secondary_niches"))
+ if sec_overlap > 0:
+ niche_pts += min(0.3, 0.15 * sec_overlap)
+ reason_bits.append("overlap in secondary niches")
+ score += niche_pts * 25
+
+ # Content types (15)
+ type_overlap = list_overlap(me_types, c.get("content_types"))
+ if type_overlap > 0:
+ score += min(1.0, type_overlap / 3.0) * 15
+ reason_bits.append("compatible content types")
+
+ # Openness alignment (10)
+ collab_overlap = list_overlap(me_collab, c.get("collaboration_types"))
+ if collab_overlap > 0:
+ score += min(1.0, collab_overlap / 2.0) * 10
+ reason_bits.append("open to similar collaboration types")
+
+ # Audience proximity (15)
+ prox = followers_proximity(me_followers, c.get("total_followers"))
+ score += prox * 15
+ if prox > 0.7:
+ reason_bits.append("similar audience scale")
+
+ # Engagement quality (15)
+ eng = normalize_percent(c.get("engagement_rate"))
+ score += eng * 10
+ avg_views = c.get("average_views") or 0
+ # Normalize avg_views relative to total_followers if available
+ if c.get("total_followers"):
+ view_ratio = min(1.0, (avg_views / max(1, c["total_followers"])) * 5)
+ score += view_ratio * 5
+ if eng > 0.6:
+ reason_bits.append("strong engagement")
+
+ # Recency/consistency (8)
+ score += recency_score(c.get("last_active_at")) * 8
+
+ # Experience/professionalism (6)
+ exp = c.get("years_of_experience") or 0
+ exp_norm = min(1.0, exp / 5.0)
+ has_kit = 1.0 if c.get("media_kit_url") else 0.0
+ score += (0.7 * exp_norm + 0.3 * has_kit) * 6
+
+ # Geo/language fit (6)
+ lang_overlap = list_overlap(me_langs, c.get("content_language"))
+ score += min(1.0, lang_overlap / 2.0) * 6
+ if lang_overlap > 0:
+ reason_bits.append("language fit")
+
+ reason = ", ".join(reason_bits) or "high potential match"
+ scored.append((c, score, reason))
+
+ # Sort by score
+ scored.sort(key=lambda x: x[1], reverse=True)
+
+ # Diversity: limit to 2 per niche in the top picks
+ picks: List[Tuple[dict, float, str]] = []
+ niche_counts = {}
+ for c, s, r in scored:
+ niche = c.get("primary_niche") or "other"
+ if niche_counts.get(niche, 0) >= 2 and len(picks) >= 2:
+ continue
+ picks.append((c, s, r))
+ niche_counts[niche] = niche_counts.get(niche, 0) + 1
+ if len(picks) >= max(12, limit * 3):
+ break
+
+ # Optional Groq reranking to refine reasons and ordering
+ if use_ai and settings.groq_api_key:
+ try:
+ groq = Groq(api_key=settings.groq_api_key)
+ def compact_profile(p: dict) -> dict:
+ return {
+ "id": p.get("id"),
+ "name": p.get("display_name"),
+ "primary_niche": p.get("primary_niche"),
+ "secondary_niches": p.get("secondary_niches") or [],
+ "content_types": p.get("content_types") or [],
+ "collaboration_types": p.get("collaboration_types") or [],
+ "total_followers": p.get("total_followers") or 0,
+ "engagement_rate": p.get("engagement_rate") or 0,
+ "average_views": p.get("average_views") or 0,
+ "languages": p.get("content_language") or [],
+ }
+ payload = {
+ "me": compact_profile(me),
+ "candidates": [
+ {
+ "candidate": compact_profile(c),
+ "rule_score": s,
+ "rule_reason": r
+ } for c, s, r in picks
+ ]
+ }
+ prompt = (
+ "You are ranking creators for collaboration potential. "
+ "Rerank candidates considering niche fit, content compatibility, audience synergy, "
+ "and complementary strengths. Return JSON array of top items with fields: "
+ "[{id, reason, adjustment (number between -10 and +10)}]. "
+ "Keep reasons concise and actionable. Only return JSON."
+ f"\nINPUT JSON:\n{payload}"
+ )
+ completion = groq.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {"role": "system", "content": "Return only JSON."},
+ {"role": "user", "content": prompt},
+ ],
+ temperature=0.5,
+ max_completion_tokens=800,
+ top_p=1,
+ stream=False,
+ )
+ content = completion.choices[0].message.content if completion.choices else "[]"
+ content = content.strip()
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ ai_items = []
+ try:
+ ai_items = json.loads(content)
+ except Exception:
+ ai_items = []
+
+ # Build map from id to adjustment and reason
+ adj = {item.get("id"): (float(item.get("adjustment", 0)), item.get("reason", "")) for item in ai_items if item.get("id")}
+ new_list = []
+ for c, s, r in picks:
+ cid = c.get("id")
+ if cid in adj:
+ add, rr = adj[cid]
+ new_list.append((c, s + add, rr or r))
+ else:
+ new_list.append((c, s, r))
+ picks = new_list
+ picks.sort(key=lambda x: x[1], reverse=True)
+ except Exception:
+ # If AI rerank fails, continue with rules-based ranking
+ pass
+
+ # Finalize top 'limit'
+ final = picks[:limit]
+ results: List[CreatorRecommendation] = []
+ for c, s, r in final:
+ platforms = []
+ if c.get("youtube_handle"): platforms.append("YouTube")
+ if c.get("instagram_handle"): platforms.append("Instagram")
+ if c.get("tiktok_handle"): platforms.append("TikTok")
+ if c.get("twitter_handle"): platforms.append("Twitter")
+ results.append(CreatorRecommendation(
+ id=c["id"],
+ display_name=c.get("display_name", "Unknown"),
+ profile_picture_url=c.get("profile_picture_url"),
+ primary_niche=c.get("primary_niche"),
+ total_followers=c.get("total_followers"),
+ engagement_rate=c.get("engagement_rate"),
+ top_platforms=platforms[:3] or None,
+ match_score=round(s, 2),
+ reason=r
+ ))
+ return results
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error generating recommendations: {str(e)}"
+ ) from e
+
+
+@router.get("/creators/{creator_id}", response_model=CreatorFullResponse)
+async def get_creator_details(
+ creator_id: str,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Get full details of a specific creator.
+
+ - **creator_id**: The creator ID
+ """
+ supabase = supabase_anon
+
+ try:
+ # Fetch creator details
+ response = supabase.table("creators") \
+ .select("*") \
+ .eq("id", creator_id) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(
+ status_code=404,
+ detail="Creator not found"
+ )
+
+ return response.data
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ if "PGRST116" in str(e): # No rows returned
+ raise HTTPException(
+ status_code=404,
+ detail="Creator not found"
+ ) from e
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching creator: {str(e)}"
+ ) from e
+
+
+@router.get("/creators/niches/list")
+async def list_niches(
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Get list of all unique primary niches for filtering.
+ """
+ supabase = supabase_anon
+
+ try:
+ # Get all unique primary niches
+ response = supabase.table("creators") \
+ .select("primary_niche") \
+ .eq("is_active", True) \
+ .execute()
+
+ creators = response.data if response.data else []
+
+ # Extract unique niches
+ niches = sorted(set(c.get("primary_niche") for c in creators if c.get("primary_niche")))
+
+ return {"niches": niches}
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching niches: {str(e)}"
+ ) from e
+
diff --git a/backend/app/api/routes/gemini_generate.py b/backend/app/api/routes/gemini_generate.py
new file mode 100644
index 0000000..c8b5093
--- /dev/null
+++ b/backend/app/api/routes/gemini_generate.py
@@ -0,0 +1,39 @@
+import os
+import httpx
+from fastapi import APIRouter, HTTPException
+from pydantic import BaseModel
+from app.core.config import settings
+
+
+
+
+
+
+
+router = APIRouter()
+GEMINI_API_KEY = settings.gemini_api_key
+GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
+
+class GenerateRequest(BaseModel):
+ prompt: str
+
+@router.post("/generate")
+async def generate_content(request: GenerateRequest):
+ if not GEMINI_API_KEY:
+ raise HTTPException(status_code=500, detail="Gemini API is not configured. Please set GEMINI_API_KEY in environment.")
+ payload = {
+ "contents": [{"role": "user", "parts": [{"text": request.prompt}]}]
+ }
+ headers = {
+ "Content-Type": "application/json",
+ }
+ params = {"key": GEMINI_API_KEY}
+ try:
+ async with httpx.AsyncClient(timeout=30.0) as client:
+ response = await client.post(GEMINI_API_URL, json=payload, headers=headers, params=params)
+ response.raise_for_status()
+ return response.json()
+ except httpx.RequestError as e:
+ raise HTTPException(status_code=502, detail=f"Gemini API error: {str(e)}")
+ except httpx.HTTPStatusError as e:
+ raise HTTPException(status_code=502, detail=f"Gemini API error: {str(e)}")
diff --git a/backend/app/api/routes/groq_generate.py b/backend/app/api/routes/groq_generate.py
new file mode 100644
index 0000000..e114338
--- /dev/null
+++ b/backend/app/api/routes/groq_generate.py
@@ -0,0 +1,43 @@
+import os
+import requests
+from fastapi import APIRouter, HTTPException, Request
+from pydantic import BaseModel
+from app.core.config import settings
+
+router = APIRouter()
+
+class GroqRequest(BaseModel):
+ prompt: str
+ model: str = "meta-llama/llama-4-scout-17b-16e-instruct" # default, can be overridden
+ max_tokens: int = 256
+ temperature: float = 0.7
+
+@router.post("/groq/generate")
+async def generate_groq_response(data: GroqRequest, request: Request):
+ api_key = settings.groq_api_key
+ if not api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured.")
+ headers = {
+ "Authorization": f"Bearer {api_key}",
+ "Content-Type": "application/json"
+ }
+ payload = {
+ "model": data.model,
+ "messages": [
+ {"role": "user", "content": data.prompt}
+ ],
+ "max_tokens": data.max_tokens,
+ "temperature": data.temperature
+ }
+ try:
+ response = requests.post(
+ "https://api.groq.com/openai/v1/chat/completions",
+ headers=headers,
+ json=payload,
+ timeout=30
+ )
+ response.raise_for_status()
+ result = response.json()
+ return {"result": result}
+ except requests.RequestException as e:
+ raise HTTPException(status_code=502, detail=f"GROQ API error: {str(e)}")
diff --git a/backend/app/api/routes/health.py b/backend/app/api/routes/health.py
new file mode 100644
index 0000000..560eb49
--- /dev/null
+++ b/backend/app/api/routes/health.py
@@ -0,0 +1,51 @@
+"""
+Health check routes for monitoring service status
+"""
+from fastapi import APIRouter, HTTPException
+
+router = APIRouter(prefix="/health", tags=["health"])
+
+@router.get("/")
+def health_check():
+ """Basic health check endpoint"""
+ return {"status": "healthy", "message": "Backend is running"}
+
+@router.get("/supabase")
+def check_supabase():
+ """
+ Check Supabase connection status.
+ This endpoint attempts to query Supabase to verify the connection.
+ """
+ try:
+ from app.core.supabase_clients import supabase_anon as supabase
+
+ # Attempt a simple query to verify connection
+ response = supabase.table("_supabase_test").select("*").limit(1).execute()
+
+ return {
+ "connected": True,
+ "message": "Supabase connection is working!",
+ "status": "healthy"
+ }
+ except Exception as e:
+ error_msg = str(e)
+ # Detect table-not-found error (Supabase/PostgREST or DB error)
+ if (
+ "does not exist" in error_msg or
+ "relation" in error_msg and "does not exist" in error_msg or
+ "Could not find the table" in error_msg or
+ "PGRST205" in error_msg
+ ):
+ return {
+ "connected": True,
+ "message": "Supabase client initialized (no tables queried yet)",
+ "status": "ready",
+ "note": error_msg
+ }
+ # For any other error, treat as unhealthy
+ return {
+ "connected": False,
+ "message": "Supabase connection failed",
+ "status": "unhealthy",
+ "note": error_msg
+ }
diff --git a/backend/app/api/routes/profiles.py b/backend/app/api/routes/profiles.py
new file mode 100644
index 0000000..3aa4142
--- /dev/null
+++ b/backend/app/api/routes/profiles.py
@@ -0,0 +1,633 @@
+"""
+Profile management routes for brands and creators
+"""
+
+import httpx
+import json
+from fastapi import APIRouter, HTTPException, Depends
+from pydantic import BaseModel
+from typing import Optional, Dict, Any
+from app.core.supabase_clients import supabase_anon
+from app.core.dependencies import get_current_user, get_current_brand, get_current_creator
+from app.core.config import settings
+
+router = APIRouter()
+GEMINI_API_KEY = settings.gemini_api_key
+GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
+
+
+class ProfileUpdateRequest(BaseModel):
+ """Generic profile update request - accepts any fields"""
+ data: Dict[str, Any]
+
+
+def calculate_brand_completion_percentage(brand: dict) -> int:
+ """Calculate profile completion percentage for a brand"""
+ required_fields = [
+ 'company_name', 'industry', 'website_url', 'company_description',
+ 'company_logo_url', 'contact_email', 'contact_phone'
+ ]
+
+ important_fields = [
+ 'company_tagline', 'headquarters_location', 'company_size',
+ 'target_audience_description', 'brand_values', 'brand_personality',
+ 'marketing_goals', 'preferred_platforms', 'monthly_marketing_budget'
+ ]
+
+ nice_to_have_fields = [
+ 'company_cover_image_url', 'social_media_links', 'founded_year',
+ 'brand_voice', 'campaign_types_interested', 'preferred_content_types'
+ ]
+
+ completed = 0
+ total = len(required_fields) + len(important_fields) + len(nice_to_have_fields)
+
+ # Required fields (weight: 3x)
+ for field in required_fields:
+ if brand.get(field):
+ completed += 3
+
+ # Important fields (weight: 2x)
+ for field in important_fields:
+ if brand.get(field):
+ completed += 2
+
+ # Nice to have fields (weight: 1x)
+ for field in nice_to_have_fields:
+ if brand.get(field):
+ completed += 1
+
+ # Calculate percentage
+ max_score = len(required_fields) * 3 + len(important_fields) * 2 + len(nice_to_have_fields)
+ percentage = int((completed / max_score) * 100) if max_score > 0 else 0
+ return min(100, max(0, percentage))
+
+
+def calculate_creator_completion_percentage(creator: dict) -> int:
+ """Calculate profile completion percentage for a creator"""
+ required_fields = [
+ 'display_name', 'primary_niche', 'profile_picture_url', 'bio'
+ ]
+
+ important_fields = [
+ 'tagline', 'website_url', 'instagram_handle', 'youtube_handle',
+ 'content_types', 'collaboration_types', 'rate_per_post',
+ 'years_of_experience', 'posting_frequency'
+ ]
+
+ nice_to_have_fields = [
+ 'cover_image_url', 'secondary_niches', 'content_language',
+ 'portfolio_links', 'media_kit_url', 'equipment_quality',
+ 'editing_software', 'preferred_payment_terms'
+ ]
+
+ completed = 0
+ total = len(required_fields) + len(important_fields) + len(nice_to_have_fields)
+
+ # Required fields (weight: 3x)
+ for field in required_fields:
+ if creator.get(field):
+ completed += 3
+
+ # Important fields (weight: 2x)
+ for field in important_fields:
+ if creator.get(field):
+ completed += 2
+
+ # Nice to have fields (weight: 1x)
+ for field in nice_to_have_fields:
+ if creator.get(field):
+ completed += 1
+
+ # Calculate percentage
+ max_score = len(required_fields) * 3 + len(important_fields) * 2 + len(nice_to_have_fields)
+ percentage = int((completed / max_score) * 100) if max_score > 0 else 0
+ return min(100, max(0, percentage))
+
+
+@router.get("/brand/profile")
+async def get_brand_profile(
+ brand: dict = Depends(get_current_brand)
+):
+ """Get the current brand's profile"""
+ try:
+ # Calculate completion percentage
+ completion = calculate_brand_completion_percentage(brand)
+
+ # Update completion percentage in database if different
+ if brand.get('profile_completion_percentage') != completion:
+ supabase_anon.table('brands') \
+ .update({'profile_completion_percentage': completion}) \
+ .eq('id', brand['id']) \
+ .execute()
+ brand['profile_completion_percentage'] = completion
+
+ return brand
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching brand profile: {str(e)}"
+ ) from e
+
+
+@router.put("/brand/profile")
+async def update_brand_profile(
+ update_request: ProfileUpdateRequest,
+ brand: dict = Depends(get_current_brand)
+):
+ """Update the current brand's profile"""
+ try:
+ update_data = update_request.data
+
+ # Remove fields that shouldn't be updated directly
+ restricted_fields = ['id', 'user_id', 'created_at', 'is_active']
+ for field in restricted_fields:
+ update_data.pop(field, None)
+
+ # Add updated_at timestamp
+ from datetime import datetime
+ update_data['updated_at'] = datetime.utcnow().isoformat()
+
+ # Update in database
+ response = supabase_anon.table('brands') \
+ .update(update_data) \
+ .eq('id', brand['id']) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(
+ status_code=404,
+ detail="Brand profile not found"
+ )
+
+ updated_brand = response.data[0] if response.data else brand
+
+ # Recalculate completion percentage
+ completion = calculate_brand_completion_percentage(updated_brand)
+
+ # Update completion percentage
+ if updated_brand.get('profile_completion_percentage') != completion:
+ supabase_anon.table('brands') \
+ .update({'profile_completion_percentage': completion}) \
+ .eq('id', brand['id']) \
+ .execute()
+ updated_brand['profile_completion_percentage'] = completion
+
+ return updated_brand
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error updating brand profile: {str(e)}"
+ ) from e
+
+
+@router.get("/creator/profile")
+async def get_creator_profile(
+ creator: dict = Depends(get_current_creator)
+):
+ """Get the current creator's profile"""
+ try:
+ # Calculate completion percentage
+ completion = calculate_creator_completion_percentage(creator)
+
+ # Update completion percentage in database if different
+ if creator.get('profile_completion_percentage') != completion:
+ supabase_anon.table('creators') \
+ .update({'profile_completion_percentage': completion}) \
+ .eq('id', creator['id']) \
+ .execute()
+ creator['profile_completion_percentage'] = completion
+
+ return creator
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching creator profile: {str(e)}"
+ ) from e
+
+
+@router.put("/creator/profile")
+async def update_creator_profile(
+ update_request: ProfileUpdateRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """Update the current creator's profile"""
+ try:
+ update_data = update_request.data
+
+ # Remove fields that shouldn't be updated directly
+ restricted_fields = ['id', 'user_id', 'created_at', 'is_active']
+ for field in restricted_fields:
+ update_data.pop(field, None)
+
+ # Add updated_at timestamp
+ from datetime import datetime
+ update_data['updated_at'] = datetime.utcnow().isoformat()
+
+ # Update in database
+ response = supabase_anon.table('creators') \
+ .update(update_data) \
+ .eq('id', creator['id']) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(
+ status_code=404,
+ detail="Creator profile not found"
+ )
+
+ updated_creator = response.data[0] if response.data else creator
+
+ # Recalculate completion percentage
+ completion = calculate_creator_completion_percentage(updated_creator)
+
+ # Update completion percentage
+ if updated_creator.get('profile_completion_percentage') != completion:
+ supabase_anon.table('creators') \
+ .update({'profile_completion_percentage': completion}) \
+ .eq('id', creator['id']) \
+ .execute()
+ updated_creator['profile_completion_percentage'] = completion
+
+ return updated_creator
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error updating creator profile: {str(e)}"
+ ) from e
+
+
+class AIFillRequest(BaseModel):
+ """Request for AI profile filling"""
+ user_input: str
+ context: Optional[Dict[str, Any]] = None
+
+
+@router.post("/brand/profile/ai-fill")
+async def ai_fill_brand_profile(
+ request: AIFillRequest,
+ brand: dict = Depends(get_current_brand)
+):
+ """Use AI to fill brand profile based on user input"""
+ if not GEMINI_API_KEY:
+ raise HTTPException(
+ status_code=500,
+ detail="Gemini API is not configured"
+ )
+
+ try:
+ # Build prompt for Gemini
+ prompt = f"""You are an expert at extracting structured data from natural language. Your task is to analyze user input and extract ALL relevant brand profile information.
+
+Current brand profile (for context - only fill fields that are empty or null):
+{json.dumps(brand, indent=2, default=str)}
+
+User provided information:
+{request.user_input}
+
+Extract and return a JSON object with ALL fields that can be confidently determined from the user input. Use these exact field names and data types:
+
+STRING fields:
+- company_name, company_tagline, company_description, company_logo_url, company_cover_image_url
+- industry, company_size, headquarters_location, company_type, website_url
+- contact_email, contact_phone, campaign_frequency, payment_terms
+- target_audience_description, brand_voice, product_price_range, product_catalog_url
+
+NUMBER fields (use numbers, not strings):
+- founded_year (integer), monthly_marketing_budget (numeric), influencer_budget_percentage (float 0-100)
+- budget_per_campaign_min (numeric), budget_per_campaign_max (numeric), typical_deal_size (numeric)
+- affiliate_commission_rate (float 0-100), minimum_followers_required (integer)
+- minimum_engagement_rate (float 0-100), exclusivity_duration_months (integer)
+- past_campaigns_count (integer), average_campaign_roi (float)
+- total_deals_posted (integer), total_deals_completed (integer), total_spent (numeric)
+- average_deal_rating (float), matching_score_base (float)
+
+BOOLEAN fields (use true/false):
+- offers_product_only_deals, offers_affiliate_programs, exclusivity_required
+- seasonal_products, business_verified, payment_verified, tax_id_verified
+- is_active, is_featured, is_verified_brand
+
+ARRAY fields (use JSON arrays of strings):
+- sub_industry, target_audience_age_groups, target_audience_gender, target_audience_locations
+- target_audience_interests, target_audience_income_level, brand_values, brand_personality
+- marketing_goals, campaign_types_interested, preferred_content_types, preferred_platforms
+- preferred_creator_niches, preferred_creator_size, preferred_creator_locations
+- content_dos, content_donts, brand_safety_requirements, competitor_brands
+- successful_partnerships, products_services, product_categories, search_keywords
+
+JSON OBJECT fields (use proper JSON objects):
+- social_media_links: {{"platform": "url", ...}}
+- brand_colors: {{"primary": "#hex", "secondary": "#hex", ...}}
+
+IMPORTANT RULES:
+1. Extract ALL fields that can be inferred from the input, not just obvious ones
+2. For arrays, extract multiple values if mentioned (e.g., "tech and finance" → ["tech", "finance"])
+3. For numbers, extract numeric values (e.g., "$50,000" → 50000, "5%" → 5.0)
+4. For booleans, infer from context (e.g., "we offer affiliate programs" → true)
+5. Only include fields that have clear values - omit uncertain fields
+6. Return ONLY valid JSON, no markdown, no explanations
+
+Return the JSON object now:"""
+
+ payload = {
+ "contents": [
+ {
+ "role": "user",
+ "parts": [{"text": prompt}]
+ }
+ ],
+ "generationConfig": {
+ "temperature": 0.1,
+ "topK": 40,
+ "topP": 0.95,
+ "maxOutputTokens": 8192,
+ "responseMimeType": "application/json"
+ }
+ }
+ headers = {"Content-Type": "application/json"}
+ params = {"key": GEMINI_API_KEY}
+
+ async with httpx.AsyncClient(timeout=60.0) as client:
+ response = await client.post(
+ GEMINI_API_URL,
+ json=payload,
+ headers=headers,
+ params=params
+ )
+ response.raise_for_status()
+ result = response.json()
+
+ # Extract text from Gemini response
+ text_content = ""
+ if result.get("candidates") and len(result["candidates"]) > 0:
+ parts = result["candidates"][0].get("content", {}).get("parts", [])
+ if parts:
+ # Check if response is already JSON (when responseMimeType is set)
+ if "text" in parts[0]:
+ text_content = parts[0].get("text", "")
+ else:
+ # Fallback for other response formats
+ text_content = json.dumps(parts[0])
+
+ # Parse JSON from response
+ try:
+ # Remove markdown code blocks if present
+ if text_content.startswith("```"):
+ # Find the closing ```
+ parts = text_content.split("```")
+ if len(parts) >= 3:
+ text_content = parts[1]
+ if text_content.startswith("json"):
+ text_content = text_content[4:]
+ else:
+ text_content = text_content[3:]
+ text_content = text_content.strip()
+
+ # Try to find JSON object in the response
+ if not text_content.startswith("{"):
+ # Try to extract JSON from the text
+ start_idx = text_content.find("{")
+ end_idx = text_content.rfind("}")
+ if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
+ text_content = text_content[start_idx:end_idx+1]
+
+ extracted_data = json.loads(text_content)
+
+ # Merge with existing profile (don't overwrite existing non-null values)
+ update_data = {}
+ for key, value in extracted_data.items():
+ # Skip null, empty strings, and empty arrays/objects
+ if value is None:
+ continue
+ if isinstance(value, str) and value.strip() == "":
+ continue
+ if isinstance(value, list) and len(value) == 0:
+ continue
+ if isinstance(value, dict) and len(value) == 0:
+ continue
+
+ # Check if field exists and has a value in current profile
+ current_value = brand.get(key)
+ should_update = False
+
+ if current_value is None or current_value == "":
+ should_update = True
+ elif isinstance(current_value, list) and len(current_value) == 0:
+ should_update = True
+ elif isinstance(current_value, dict) and len(current_value) == 0:
+ should_update = True
+ elif isinstance(current_value, bool) and not current_value and isinstance(value, bool) and value:
+ # Allow updating false booleans to true
+ should_update = True
+
+ if should_update:
+ update_data[key] = value
+
+ if not update_data:
+ return {"message": "No new data could be extracted", "data": {}}
+
+ return {"message": f"Profile data extracted successfully. {len(update_data)} fields updated.", "data": update_data}
+ except json.JSONDecodeError as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to parse AI response as JSON: {str(e)}. Response: {text_content[:200]}"
+ )
+ except httpx.RequestError as e:
+ raise HTTPException(
+ status_code=502,
+ detail=f"Gemini API error: {str(e)}"
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error generating profile data: {str(e)}"
+ ) from e
+
+
+@router.post("/creator/profile/ai-fill")
+async def ai_fill_creator_profile(
+ request: AIFillRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """Use AI to fill creator profile based on user input"""
+ if not GEMINI_API_KEY:
+ raise HTTPException(
+ status_code=500,
+ detail="Gemini API is not configured"
+ )
+
+ try:
+ # Build prompt for Gemini
+ prompt = f"""You are an expert at extracting structured data from natural language. Your task is to analyze user input and extract ALL relevant creator profile information.
+
+Current creator profile (for context - only fill fields that are empty or null):
+{json.dumps(creator, indent=2, default=str)}
+
+User provided information:
+{request.user_input}
+
+Extract and return a JSON object with ALL fields that can be confidently determined from the user input. Use these exact field names and data types:
+
+STRING fields:
+- display_name, tagline, bio, profile_picture_url, cover_image_url, website_url
+- youtube_url, youtube_handle, instagram_url, instagram_handle, tiktok_url, tiktok_handle
+- twitter_url, twitter_handle, twitch_url, twitch_handle, linkedin_url, facebook_url
+- audience_age_primary, posting_frequency, best_performing_content_type, equipment_quality
+- preferred_payment_terms, media_kit_url
+
+NUMBER fields (use numbers, not strings):
+- youtube_subscribers (integer), instagram_followers (integer), tiktok_followers (integer)
+- twitter_followers (integer), twitch_followers (integer)
+- total_followers (integer), total_reach (integer), average_views (integer)
+- engagement_rate (float 0-100), average_engagement_per_post (integer)
+- years_of_experience (integer), team_size (integer)
+- rate_per_post (numeric), rate_per_video (numeric), rate_per_story (numeric), rate_per_reel (numeric)
+- minimum_deal_value (numeric), matching_score_base (float)
+
+BOOLEAN fields (use true/false):
+- content_creation_full_time, rate_negotiable, accepts_product_only_deals
+- email_verified, phone_verified, identity_verified
+- is_active, is_featured, is_verified_creator
+
+ARRAY fields (use JSON arrays of strings):
+- secondary_niches, content_types, content_language, audience_age_secondary
+- audience_interests, editing_software, collaboration_types
+- preferred_brands_style, not_interested_in, portfolio_links
+- past_brand_collaborations, case_study_links, search_keywords
+
+JSON OBJECT fields (use proper JSON objects):
+- audience_gender_split: {{"male": 45, "female": 50, "other": 5}} (percentages)
+- audience_locations: {{"country": "percentage", ...}} or {{"city": "percentage", ...}}
+- peak_posting_times: {{"monday": ["09:00", "18:00"], ...}} or {{"day": "time", ...}}
+- social_platforms: {{"platform": {{"handle": "...", "followers": 12345}}, ...}}
+
+IMPORTANT RULES:
+1. Extract ALL fields that can be inferred from the input, not just obvious ones
+2. For arrays, extract multiple values if mentioned (e.g., "lifestyle and tech" → ["lifestyle", "tech"])
+3. For numbers, extract numeric values (e.g., "$500 per post" → 500, "5 years" → 5)
+4. For booleans, infer from context (e.g., "I do this full-time" → true)
+5. For social media, extract handles, URLs, and follower counts if mentioned
+6. For audience data, structure as JSON objects with appropriate keys
+7. Only include fields that have clear values - omit uncertain fields
+8. Return ONLY valid JSON, no markdown, no explanations
+
+Return the JSON object now:"""
+
+ payload = {
+ "contents": [
+ {
+ "role": "user",
+ "parts": [{"text": prompt}]
+ }
+ ],
+ "generationConfig": {
+ "temperature": 0.1,
+ "topK": 40,
+ "topP": 0.95,
+ "maxOutputTokens": 8192,
+ "responseMimeType": "application/json"
+ }
+ }
+ headers = {"Content-Type": "application/json"}
+ params = {"key": GEMINI_API_KEY}
+
+ async with httpx.AsyncClient(timeout=60.0) as client:
+ response = await client.post(
+ GEMINI_API_URL,
+ json=payload,
+ headers=headers,
+ params=params
+ )
+ response.raise_for_status()
+ result = response.json()
+
+ # Extract text from Gemini response
+ text_content = ""
+ if result.get("candidates") and len(result["candidates"]) > 0:
+ parts = result["candidates"][0].get("content", {}).get("parts", [])
+ if parts:
+ # Check if response is already JSON (when responseMimeType is set)
+ if "text" in parts[0]:
+ text_content = parts[0].get("text", "")
+ else:
+ # Fallback for other response formats
+ text_content = json.dumps(parts[0])
+
+ # Parse JSON from response
+ try:
+ # Remove markdown code blocks if present
+ if text_content.startswith("```"):
+ # Find the closing ```
+ parts = text_content.split("```")
+ if len(parts) >= 3:
+ text_content = parts[1]
+ if text_content.startswith("json"):
+ text_content = text_content[4:]
+ else:
+ text_content = text_content[3:]
+ text_content = text_content.strip()
+
+ # Try to find JSON object in the response
+ if not text_content.startswith("{"):
+ # Try to extract JSON from the text
+ start_idx = text_content.find("{")
+ end_idx = text_content.rfind("}")
+ if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
+ text_content = text_content[start_idx:end_idx+1]
+
+ extracted_data = json.loads(text_content)
+
+ # Merge with existing profile (don't overwrite existing non-null values)
+ update_data = {}
+ for key, value in extracted_data.items():
+ # Skip null, empty strings, and empty arrays/objects
+ if value is None:
+ continue
+ if isinstance(value, str) and value.strip() == "":
+ continue
+ if isinstance(value, list) and len(value) == 0:
+ continue
+ if isinstance(value, dict) and len(value) == 0:
+ continue
+
+ # Check if field exists and has a value in current profile
+ current_value = creator.get(key)
+ should_update = False
+
+ if current_value is None or current_value == "":
+ should_update = True
+ elif isinstance(current_value, list) and len(current_value) == 0:
+ should_update = True
+ elif isinstance(current_value, dict) and len(current_value) == 0:
+ should_update = True
+ elif isinstance(current_value, bool) and not current_value and isinstance(value, bool) and value:
+ # Allow updating false booleans to true
+ should_update = True
+
+ if should_update:
+ update_data[key] = value
+
+ if not update_data:
+ return {"message": "No new data could be extracted", "data": {}}
+
+ return {"message": f"Profile data extracted successfully. {len(update_data)} fields updated.", "data": update_data}
+ except json.JSONDecodeError as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to parse AI response as JSON: {str(e)}. Response: {text_content[:200]}"
+ )
+ except httpx.RequestError as e:
+ raise HTTPException(
+ status_code=502,
+ detail=f"Gemini API error: {str(e)}"
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error generating profile data: {str(e)}"
+ ) from e
+
diff --git a/backend/app/api/routes/proposals.py b/backend/app/api/routes/proposals.py
new file mode 100644
index 0000000..a54b965
--- /dev/null
+++ b/backend/app/api/routes/proposals.py
@@ -0,0 +1,4736 @@
+from fastapi import APIRouter, HTTPException, Depends, Query
+from pydantic import BaseModel, Field
+from typing import Optional, List, Dict, Any
+from datetime import datetime, timezone
+from app.core.supabase_clients import supabase_anon
+from app.core.dependencies import get_current_brand, get_current_creator, get_current_user
+import json
+import re
+from groq import Groq
+from app.core.config import settings
+from postgrest.exceptions import APIError
+from fastapi import status as http_status
+from uuid import uuid4
+
+router = APIRouter()
+
+
+# Delete a proposal (brand only)
+@router.delete("/proposals/{proposal_id}", status_code=204)
+async def delete_proposal(
+ proposal_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Delete a proposal. Only the brand who owns the proposal can delete it.
+ """
+ supabase = supabase_anon
+ brand_id = brand['id']
+ try:
+ # Check if proposal exists and belongs to this brand
+ prop_resp = supabase.table("proposals") \
+ .select("id, brand_id") \
+ .eq("id", proposal_id) \
+ .single() \
+ .execute()
+ if not prop_resp.data:
+ raise HTTPException(status_code=404, detail="Proposal not found")
+ if prop_resp.data["brand_id"] != brand_id:
+ raise HTTPException(status_code=403, detail="You do not have permission to delete this proposal")
+
+ # Delete the proposal
+ del_resp = supabase.table("proposals") \
+ .delete() \
+ .eq("id", proposal_id) \
+ .execute()
+ if del_resp.status_code >= 400:
+ raise HTTPException(status_code=500, detail=f"Failed to delete proposal: {del_resp.data}")
+ return
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error deleting proposal: {str(e)}")
+"""
+Proposals management routes for brands and creators.
+"""
+
+
+
+class ProposalCreate(BaseModel):
+ """Schema for creating a new proposal."""
+ campaign_id: str
+ creator_id: Optional[str] = None # Optional: required for brands, auto-filled for creators
+ subject: str = Field(..., min_length=1, max_length=255)
+ message: str = Field(..., min_length=1)
+ proposed_amount: Optional[float] = None
+ content_ideas: Optional[List[str]] = Field(default_factory=list)
+ ideal_pricing: Optional[str] = None
+
+
+class ProposalResponse(BaseModel):
+ """Schema for proposal response."""
+ id: str
+ campaign_id: str
+ brand_id: str
+ creator_id: str
+ subject: str
+ message: str
+ proposed_amount: Optional[float]
+ content_ideas: Optional[List[str]]
+ ideal_pricing: Optional[str]
+ status: str
+ created_at: datetime
+ updated_at: datetime
+ campaign_title: Optional[str] = None
+ brand_name: Optional[str] = None
+ creator_name: Optional[str] = None
+ negotiation_status: Optional[str] = None
+ negotiation_thread: Optional[List[Dict[str, Any]]] = None
+ current_terms: Optional[Dict[str, Any]] = None
+ version: Optional[int] = None
+ contract_id: Optional[str] = None
+
+
+class ContractResponse(BaseModel):
+ """Schema for contract response."""
+ id: str
+ proposal_id: str
+ brand_id: str
+ creator_id: str
+ terms: Dict[str, Any]
+ status: str
+ created_at: datetime
+ updated_at: datetime
+ brand_name: Optional[str] = None
+ creator_name: Optional[str] = None
+ proposal: Optional[ProposalResponse] = None
+ negotiation_thread: Optional[List[Dict[str, Any]]] = None
+ unsigned_contract_link: Optional[str] = None
+ signed_contract_link: Optional[str] = None
+ unsigned_contract_downloaded_by_creator: Optional[bool] = False
+ signed_contract_downloaded_by_brand: Optional[bool] = False
+ pending_status_change: Optional[Dict[str, Any]] = None
+
+
+class AcceptNegotiationResponse(BaseModel):
+ proposal: ProposalResponse
+ contract: ContractResponse
+
+
+class ProposalStatusUpdate(BaseModel):
+ """Schema for updating proposal status."""
+ status: str = Field(..., pattern="^(pending|accepted|declined|withdrawn)$")
+
+
+def sanitize_content_ideas(value):
+ """Ensure content_ideas is always a list of strings."""
+ import json
+ if isinstance(value, str):
+ try:
+ value = json.loads(value)
+ except Exception:
+ value = []
+ if not isinstance(value, list):
+ value = []
+ # Ensure all items are strings
+ sanitized = [str(item) for item in value if item is not None]
+ if not sanitized:
+ return None
+ return sanitized
+
+def clean_message_field(msg: str) -> str:
+ """Clean message field if it contains JSON string."""
+ if not msg or not isinstance(msg, str):
+ return msg or ""
+
+ if not msg.strip().startswith('{'):
+ return msg
+
+ try:
+ # Try to parse as JSON, handling escaped strings
+ msg_clean = msg.replace('\\\\n', '\n').replace('\\\\"', '"')
+ msg_obj = json.loads(msg_clean)
+ if isinstance(msg_obj, dict) and 'message' in msg_obj:
+ return msg_obj['message']
+ except Exception:
+ pass
+
+ try:
+ # Try to extract message directly using regex
+ match = re.search(r'"message"\s*:\s*"([^"]*(?:\\.[^"]*)*)"', msg)
+ if match:
+ extracted_msg = match.group(1)
+ extracted_msg = extracted_msg.replace('\\n', '\n').replace('\\"', '"').replace('\\\\', '\\')
+ return extracted_msg
+ except Exception:
+ pass
+
+ return msg
+
+
+def parse_datetime(dt_str: str) -> datetime:
+ """Parse datetime string to datetime object."""
+ if isinstance(dt_str, datetime):
+ return dt_str
+
+ try:
+ # Handle ISO format with or without timezone
+ if dt_str.endswith('Z'):
+ dt_str = dt_str[:-1] + '+00:00'
+ return datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
+ except Exception:
+ return datetime.now(timezone.utc)
+
+
+def normalize_json_field(value, default):
+ """Ensure JSON fields are returned as the expected Python type."""
+ if value is None:
+ return default
+ if isinstance(value, (dict, list)):
+ return value
+ if isinstance(value, str):
+ try:
+ parsed = json.loads(value)
+ if isinstance(parsed, type(default)):
+ return parsed
+ return default
+ except Exception:
+ return default
+ return default
+
+
+def normalize_negotiation_thread(thread) -> List[Dict[str, Any]]:
+ """Normalize negotiation thread to a list of dict entries."""
+ normalized = normalize_json_field(thread, [])
+ if not isinstance(normalized, list):
+ return []
+ cleaned = []
+ for entry in normalized:
+ if not isinstance(entry, dict):
+ continue
+ cleaned.append(entry)
+ return cleaned
+
+
+def normalize_current_terms(terms) -> Dict[str, Any]:
+ """Normalize current terms to a dict."""
+ normalized = normalize_json_field(terms, {})
+ if not isinstance(normalized, dict):
+ return {}
+ return normalized
+
+
+def normalize_proposal_record(
+ prop: dict,
+ brand_name: Optional[str] = None,
+ creator_name: Optional[str] = None
+) -> dict:
+ """Normalize raw proposal data into consistent response dict."""
+ proposal = dict(prop)
+
+ # Clean message field
+ if proposal.get("message"):
+ proposal["message"] = clean_message_field(proposal["message"])
+
+ # Parse datetimes
+ if proposal.get("created_at"):
+ proposal["created_at"] = parse_datetime(proposal["created_at"])
+ if proposal.get("updated_at"):
+ proposal["updated_at"] = parse_datetime(proposal["updated_at"])
+
+ # Convert proposed_amount to float if needed
+ if proposal.get("proposed_amount") is not None:
+ try:
+ proposal["proposed_amount"] = float(proposal["proposed_amount"])
+ except (TypeError, ValueError):
+ proposal["proposed_amount"] = None
+
+ # Sanitize content ideas
+ sanitized_content_ideas = sanitize_content_ideas(proposal.get("content_ideas"))
+ proposal["content_ideas"] = sanitized_content_ideas
+
+ # Negotiation fields
+ proposal["negotiation_status"] = proposal.get("negotiation_status", "none")
+ proposal["negotiation_thread"] = normalize_negotiation_thread(
+ proposal.get("negotiation_thread")
+ )
+ proposal["current_terms"] = normalize_current_terms(
+ proposal.get("current_terms")
+ )
+ proposal["version"] = proposal.get("version", 1)
+ proposal["contract_id"] = proposal.get("contract_id")
+
+ if brand_name:
+ proposal["brand_name"] = brand_name
+ if creator_name:
+ proposal["creator_name"] = creator_name
+
+ # Remove nested joins to avoid leaking raw data structures
+ proposal.pop("campaigns", None)
+ proposal.pop("creators", None)
+ proposal.pop("brands", None)
+
+ return proposal
+
+
+def normalize_contract_record(contract: dict) -> dict:
+ """Normalize raw contract data."""
+ if not contract:
+ return {}
+
+ record = dict(contract)
+
+ if record.get("created_at"):
+ record["created_at"] = parse_datetime(record["created_at"])
+ if record.get("updated_at"):
+ record["updated_at"] = parse_datetime(record["updated_at"])
+
+ record["terms"] = normalize_current_terms(record.get("terms"))
+
+ proposal_data = None
+ if isinstance(record.get("proposals"), dict):
+ proposal_data = normalize_proposal_record(record["proposals"])
+ if isinstance(record["proposals"].get("campaigns"), dict):
+ proposal_data["campaign_title"] = record["proposals"]["campaigns"].get("title")
+ if isinstance(record["proposals"].get("brands"), dict):
+ proposal_data["brand_name"] = record["proposals"]["brands"].get("company_name")
+ if isinstance(record["proposals"].get("creators"), dict):
+ proposal_data["creator_name"] = record["proposals"]["creators"].get("display_name")
+
+ record["proposal"] = proposal_data
+ record["negotiation_thread"] = (
+ proposal_data.get("negotiation_thread") if proposal_data else None
+ )
+ record["brand_name"] = record.get("brand_name") or (
+ proposal_data.get("brand_name") if proposal_data else None
+ )
+ record["creator_name"] = record.get("creator_name") or (
+ proposal_data.get("creator_name") if proposal_data else None
+ )
+
+ record.pop("proposals", None)
+
+ return record
+
+
+def build_thread_entry(
+ sender_id: str,
+ sender_role: str,
+ message: str,
+ entry_type: str = "message",
+ meta: Optional[Dict[str, Any]] = None
+) -> Dict[str, Any]:
+ """Create a standardized negotiation thread entry."""
+ entry = {
+ "id": str(uuid4()),
+ "sender_id": sender_id,
+ "sender_role": sender_role,
+ "message": message,
+ "timestamp": datetime.now(timezone.utc).isoformat(),
+ "type": entry_type,
+ }
+ if meta:
+ entry["meta"] = meta
+ return entry
+
+
+def fetch_brand_profile_by_user_id(user_id: str) -> Optional[dict]:
+ """Fetch brand profile associated with the given user ID."""
+ try:
+ response = supabase_anon.table("brands") \
+ .select("id, company_name") \
+ .eq("user_id", user_id) \
+ .single() \
+ .execute()
+ return response.data if response and response.data else None
+ except Exception:
+ return None
+
+
+def fetch_creator_profile_by_user_id(user_id: str) -> Optional[dict]:
+ """Fetch creator profile associated with the given user ID."""
+ try:
+ response = supabase_anon.table("creators") \
+ .select("id, display_name") \
+ .eq("user_id", user_id) \
+ .single() \
+ .execute()
+ return response.data if response and response.data else None
+ except Exception:
+ return None
+
+
+def fetch_proposal_with_joins(proposal_id: str) -> dict:
+ """Fetch a proposal with joined campaign, brand, and creator data, handling serialization errors."""
+ supabase = supabase_anon
+
+ try:
+ # Try to fetch with joins
+ proposal_resp = supabase.table("proposals") \
+ .select("id,campaign_id,brand_id,creator_id,subject,message,proposed_amount,content_ideas,ideal_pricing,status,created_at,updated_at,negotiation_status,negotiation_thread,current_terms,version,contract_id, campaigns(title), brands(company_name), creators(display_name)") \
+ .eq("id", proposal_id) \
+ .single() \
+ .execute()
+ if proposal_resp.data:
+ return proposal_resp.data
+ except APIError as api_error:
+ # Handle serialization error
+ error_dict = {}
+ error_details = ''
+
+ if api_error.args:
+ if isinstance(api_error.args[0], dict):
+ error_dict = api_error.args[0]
+ error_details = error_dict.get('details', '')
+ else:
+ error_str = str(api_error.args[0])
+ if "JSON could not be generated" in error_str:
+ code_match = re.search(r"'code'\s*:\s*(\d+)", error_str)
+ code_value = int(code_match.group(1)) if code_match else 0
+
+ if code_value == 200:
+ json_start = error_str.find('{"id"')
+ if json_start == -1:
+ json_start = error_str.find('{\"id\"')
+ if json_start >= 0:
+ brace_count = 0
+ json_end = json_start
+ for i in range(json_start, len(error_str)):
+ if error_str[i] == '{':
+ brace_count += 1
+ elif error_str[i] == '}':
+ brace_count -= 1
+ if brace_count == 0:
+ json_end = i + 1
+ break
+ if json_end > json_start:
+ error_details = error_str[json_start:json_end]
+ error_dict = {'code': 200, 'message': 'JSON could not be generated', 'details': error_details}
+
+ error_code = error_dict.get('code', 0) if isinstance(error_dict, dict) else 0
+ error_msg = str(error_dict.get('message', '')) if isinstance(error_dict, dict) else str(api_error)
+
+ if error_code == 200 and "JSON could not be generated" in error_msg and error_details:
+ try:
+ details_str = str(error_details)
+ if details_str.startswith("b'") and details_str.endswith("'"):
+ details_str = details_str[2:-1]
+ elif details_str.startswith("b\"") and details_str.endswith("\""):
+ details_str = details_str[2:-1]
+
+ json_start = details_str.find('{')
+ json_end = details_str.rfind('}') + 1
+ if json_start >= 0 and json_end > json_start:
+ json_str = details_str[json_start:json_end]
+ json_str = json_str.replace('\\\\\\\\', '\\')
+ json_str = json_str.replace("\\'", "'")
+ proposal_data = json.loads(json_str)
+ if proposal_data:
+ # Fetch related data separately if needed
+ return proposal_data
+ except Exception:
+ pass
+
+ # Fallback: fetch proposal without joins, then fetch related data separately
+ try:
+ proposal_resp = supabase.table("proposals") \
+ .select("id,campaign_id,brand_id,creator_id,subject,message,proposed_amount,content_ideas,ideal_pricing,status,created_at,updated_at,negotiation_status,negotiation_thread,current_terms,version,contract_id") \
+ .eq("id", proposal_id) \
+ .single() \
+ .execute()
+
+ if proposal_resp.data:
+ proposal_data = proposal_resp.data
+
+ # Fetch campaign title separately
+ try:
+ campaign_resp = supabase.table("campaigns") \
+ .select("title") \
+ .eq("id", proposal_data["campaign_id"]) \
+ .single() \
+ .execute()
+ if campaign_resp.data:
+ proposal_data["campaigns"] = {"title": campaign_resp.data.get("title")}
+ except Exception:
+ pass
+
+ # Fetch brand name separately
+ try:
+ brand_resp = supabase.table("brands") \
+ .select("company_name") \
+ .eq("id", proposal_data["brand_id"]) \
+ .single() \
+ .execute()
+ if brand_resp.data:
+ proposal_data["brands"] = {"company_name": brand_resp.data.get("company_name")}
+ except Exception:
+ pass
+
+ # Fetch creator name separately
+ try:
+ creator_resp = supabase.table("creators") \
+ .select("display_name") \
+ .eq("id", proposal_data["creator_id"]) \
+ .single() \
+ .execute()
+ if creator_resp.data:
+ proposal_data["creators"] = {"display_name": creator_resp.data.get("display_name")}
+ except Exception:
+ pass
+
+ return proposal_data
+ except Exception:
+ pass
+
+ raise HTTPException(status_code=404, detail="Proposal not found")
+
+
+def fetch_proposal_by_id(proposal_id: str) -> dict:
+ """Fetch a proposal by ID or raise 404."""
+ try:
+ proposal_resp = supabase_anon.table("proposals") \
+ .select("*") \
+ .eq("id", proposal_id) \
+ .single() \
+ .execute()
+ if not proposal_resp.data:
+ raise HTTPException(status_code=404, detail="Proposal not found")
+ return proposal_resp.data
+ except APIError as api_error:
+ # Handle case where Supabase returns code 200 but can't serialize JSON
+ # This happens when message field contains escaped characters
+ # APIError.args[0] might be a dict or a string, so we need to handle both
+ error_dict = {}
+ error_details = ''
+
+ if api_error.args:
+ if isinstance(api_error.args[0], dict):
+ error_dict = api_error.args[0]
+ error_details = error_dict.get('details', '')
+ else:
+ # args[0] is a string - parse it
+ error_str = str(api_error.args[0])
+ # Check if it contains the serialization error
+ if "JSON could not be generated" in error_str:
+ # Extract code first
+ code_match = re.search(r"'code'\s*:\s*(\d+)", error_str)
+ code_value = int(code_match.group(1)) if code_match else 0
+
+ if code_value == 200:
+ # Try to find the JSON object in the error string
+ # Look for the pattern: 'details': 'b\'{...}\''
+ # Or just find the first { and last } that contains "id"
+ json_start = error_str.find('{"id"')
+ if json_start == -1:
+ json_start = error_str.find('{\"id\"')
+ if json_start >= 0:
+ # Find the matching closing brace
+ brace_count = 0
+ json_end = json_start
+ for i in range(json_start, len(error_str)):
+ if error_str[i] == '{':
+ brace_count += 1
+ elif error_str[i] == '}':
+ brace_count -= 1
+ if brace_count == 0:
+ json_end = i + 1
+ break
+ if json_end > json_start:
+ error_details = error_str[json_start:json_end]
+ # Remove b' prefix if present in the original string context
+ if 'b\\\'' in error_str[json_start-10:json_start] or "b'" in error_str[json_start-10:json_start]:
+ # The JSON is already extracted, just need to unescape
+ pass
+
+ error_dict = {'code': 200, 'message': 'JSON could not be generated', 'details': error_details}
+
+ error_code = error_dict.get('code', 0) if isinstance(error_dict, dict) else 0
+ error_msg = str(error_dict.get('message', '')) if isinstance(error_dict, dict) else str(api_error)
+
+ if error_code == 200 and "JSON could not be generated" in error_msg:
+ # Try to extract data from error details
+ if not error_details and isinstance(error_dict, dict):
+ error_details = error_dict.get('details', '')
+
+ if error_details:
+ try:
+ # The details might be a bytes string or regular string
+ if isinstance(error_details, bytes):
+ error_details = error_details.decode('utf-8')
+
+ # Extract JSON from the details string
+ # Format is typically: b'{"id":"...",...}'
+ # or just: {"id":"...",...}
+ details_str = str(error_details)
+
+ # Remove b' prefix and trailing quote if present
+ if details_str.startswith("b'") and details_str.endswith("'"):
+ details_str = details_str[2:-1]
+ elif details_str.startswith("b\"") and details_str.endswith("\""):
+ details_str = details_str[2:-1]
+
+ # Find the JSON object in the string
+ json_start = details_str.find('{')
+ json_end = details_str.rfind('}') + 1
+ if json_start >= 0 and json_end > json_start:
+ json_str = details_str[json_start:json_end]
+ # Unescape the string - handle double-escaped characters
+ # The JSON string has \\\\n (4 backslashes + n) which needs to become \n (1 backslash + n)
+ # Replace double backslashes: \\\\ -> \
+ # This converts \\\\n to \n, \\\\" to \", etc.
+ json_str = json_str.replace('\\\\\\\\', '\\')
+ # Also handle escaped single quotes: \\' -> '
+ json_str = json_str.replace("\\'", "'")
+ # Now the string should have proper JSON escape sequences
+ # Parse the JSON
+ proposal_data = json.loads(json_str)
+ if proposal_data:
+ return proposal_data
+ except Exception as parse_error:
+ # Log the parse error for debugging but continue to fallback
+ import traceback
+ print(f"Failed to parse error details: {parse_error}")
+ print(traceback.format_exc())
+ pass
+
+ # Fallback: try fetching with specific fields to avoid serialization issues
+ try:
+ proposal_resp = supabase_anon.table("proposals") \
+ .select("id,campaign_id,brand_id,creator_id,subject,message,proposed_amount,content_ideas,ideal_pricing,status,created_at,updated_at,negotiation_status,negotiation_thread,current_terms,version,contract_id") \
+ .eq("id", proposal_id) \
+ .single() \
+ .execute()
+ if proposal_resp.data:
+ return proposal_resp.data
+ except Exception:
+ pass
+
+ # If all else fails, raise the original error
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to fetch proposal due to data serialization issue: {error_msg}"
+ )
+ else:
+ # Re-raise if it's not the serialization error
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to fetch proposal: {error_msg}"
+ )
+
+
+class NegotiationStartRequest(BaseModel):
+ initial_message: Optional[str] = Field(
+ None, min_length=1, max_length=5000
+ )
+ proposed_terms: Optional[Dict[str, Any]] = Field(
+ default=None,
+ description="Optional proposed terms in JSON format"
+ )
+
+
+class NegotiationMessageRequest(BaseModel):
+ message: str = Field(..., min_length=1, max_length=5000)
+
+
+class NegotiationTermsUpdateRequest(BaseModel):
+ terms: Dict[str, Any] = Field(
+ ...,
+ description="Updated terms provided by the brand"
+ )
+ note: Optional[str] = Field(
+ None,
+ description="Optional message to accompany the terms update",
+ max_length=5000
+ )
+
+
+class AcceptNegotiationRequest(BaseModel):
+ message: Optional[str] = Field(
+ None,
+ description="Optional message from the creator when accepting",
+ max_length=5000
+ )
+
+
+@router.post("/proposals", response_model=ProposalResponse, status_code=201)
+async def create_proposal(
+ proposal: ProposalCreate,
+ current_user: dict = Depends(get_current_user)
+):
+ """Create a new proposal. Can be created by either a brand (to a creator) or a creator (to a brand)."""
+ supabase = supabase_anon
+ user_role = current_user.get('role')
+
+ try:
+ # Determine if this is a brand or creator creating the proposal
+ if user_role == 'Brand':
+ # Brand creating proposal: they specify creator_id, brand_id from session
+ brand_resp = supabase.table("brands") \
+ .select("id, company_name") \
+ .eq("user_id", current_user['id']) \
+ .single() \
+ .execute()
+
+ if not brand_resp.data:
+ raise HTTPException(status_code=404, detail="Brand profile not found")
+
+ brand_id = brand_resp.data['id']
+ brand_name = brand_resp.data.get("company_name", "Unknown Brand")
+
+ if not proposal.creator_id:
+ raise HTTPException(status_code=400, detail="creator_id is required when creating a proposal as a brand")
+
+ creator_id = proposal.creator_id
+
+ # Verify campaign belongs to brand
+ campaign_resp = supabase.table("campaigns") \
+ .select("id, title, brand_id") \
+ .eq("id", proposal.campaign_id) \
+ .eq("brand_id", brand_id) \
+ .single() \
+ .execute()
+
+ if not campaign_resp.data:
+ raise HTTPException(status_code=404, detail="Campaign not found or does not belong to you")
+
+ elif user_role == 'Creator':
+ # Creator creating proposal: they specify campaign_id, creator_id from session, brand_id from campaign
+ creator_resp = supabase.table("creators") \
+ .select("id, display_name") \
+ .eq("user_id", current_user['id']) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ if not creator_resp.data:
+ raise HTTPException(status_code=404, detail="Creator profile not found or inactive")
+
+ creator_id = creator_resp.data['id']
+ creator_name = creator_resp.data.get("display_name", "Unknown Creator")
+
+ # Get campaign to find brand_id
+ campaign_resp = supabase.table("campaigns") \
+ .select("id, title, brand_id") \
+ .eq("id", proposal.campaign_id) \
+ .single() \
+ .execute()
+
+ if not campaign_resp.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ brand_id = campaign_resp.data.get("brand_id")
+ if not brand_id:
+ raise HTTPException(status_code=404, detail="Campaign has no associated brand")
+
+ # Get brand name
+ brand_resp = supabase.table("brands") \
+ .select("company_name") \
+ .eq("id", brand_id) \
+ .single() \
+ .execute()
+
+ brand_name = brand_resp.data.get("company_name", "Unknown Brand") if brand_resp.data else "Unknown Brand"
+
+ else:
+ raise HTTPException(status_code=403, detail="Only brands and creators can create proposals")
+
+ # For brands, verify creator exists and is active
+ if user_role == 'Brand':
+ creator_resp = supabase.table("creators") \
+ .select("id, display_name") \
+ .eq("id", creator_id) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ if not creator_resp.data:
+ raise HTTPException(status_code=404, detail="Creator not found or inactive")
+
+ creator_name = creator_resp.data.get("display_name", "Unknown Creator")
+
+ # Check if proposal already exists
+ existing = supabase.table("proposals") \
+ .select("id") \
+ .eq("campaign_id", proposal.campaign_id) \
+ .eq("creator_id", creator_id) \
+ .eq("brand_id", brand_id) \
+ .execute()
+
+ if existing.data:
+ raise HTTPException(
+ status_code=400,
+ detail="A proposal already exists for this creator and campaign"
+ )
+
+ # Clean message field
+ clean_msg = clean_message_field(proposal.message)
+
+ # Sanitize content_ideas
+ content_ideas = sanitize_content_ideas(proposal.content_ideas)
+
+ proposal_data = {
+ "campaign_id": proposal.campaign_id,
+ "brand_id": brand_id,
+ "creator_id": creator_id,
+ "subject": proposal.subject,
+ "message": clean_msg,
+ "proposed_amount": proposal.proposed_amount,
+ "content_ideas": content_ideas,
+ "ideal_pricing": proposal.ideal_pricing,
+ "status": "pending"
+ }
+
+ result = supabase.table("proposals").insert(proposal_data).execute()
+
+ if not result.data:
+ raise HTTPException(status_code=500, detail="Failed to create proposal")
+
+ proposal_obj = result.data[0]
+
+ normalized = normalize_proposal_record(
+ proposal_obj,
+ brand_name=brand_name,
+ creator_name=creator_name
+ )
+ normalized["campaign_title"] = campaign_resp.data.get("title")
+
+ return normalized
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error creating proposal: {str(e)}"
+ ) from e
+
+
+@router.get("/proposals/sent", response_model=List[ProposalResponse])
+async def get_sent_proposals(
+ brand: dict = Depends(get_current_brand),
+ status: Optional[str] = Query(None, description="Filter by status"),
+ negotiation_status: Optional[str] = Query(
+ None,
+ description="Filter by negotiation status (e.g., none, open, finalized, declined)"
+ ),
+ limit: int = Query(50, ge=1, le=100),
+ offset: int = Query(0, ge=0)
+):
+ """
+ Get all proposals for the current brand.
+ This includes proposals sent by the brand to creators AND proposals sent by creators to the brand.
+ """
+ supabase = supabase_anon
+ brand_id = brand['id']
+
+ try:
+ # Get all proposals where this brand is involved (both sent by brand and received from creators)
+ query = supabase.table("proposals") \
+ .select("*, campaigns(title), creators(display_name)") \
+ .eq("brand_id", brand_id) \
+ .order("created_at", desc=True) \
+ .range(offset, offset + limit - 1)
+
+ if status:
+ query = query.eq("status", status)
+ if negotiation_status:
+ if negotiation_status == "active":
+ query = query.neq("negotiation_status", "none")
+ else:
+ query = query.eq("negotiation_status", negotiation_status)
+
+ result = query.execute()
+
+ proposals = []
+ brand_display_name = brand.get("company_name", "Unknown Brand")
+ for prop in (result.data or []):
+ creator_name = None
+ if isinstance(prop.get("creators"), dict):
+ creator_name = prop["creators"].get("display_name")
+
+ proposal = normalize_proposal_record(
+ prop,
+ brand_name=brand_display_name,
+ creator_name=creator_name
+ )
+
+ if isinstance(prop.get("campaigns"), dict):
+ proposal["campaign_title"] = prop["campaigns"].get("title")
+
+ proposals.append(proposal)
+
+ return proposals
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching proposals: {str(e)}"
+ ) from e
+
+
+@router.get("/proposals/received", response_model=List[ProposalResponse])
+async def get_received_proposals(
+ creator: dict = Depends(get_current_creator),
+ status: Optional[str] = Query(None, description="Filter by status"),
+ negotiation_status: Optional[str] = Query(
+ None,
+ description="Filter by negotiation status (e.g., none, open, finalized, declined)"
+ ),
+ limit: int = Query(50, ge=1, le=100),
+ offset: int = Query(0, ge=0)
+):
+ """Get all proposals received by the current creator."""
+ supabase = supabase_anon
+ creator_id = creator['id']
+
+ try:
+ query = supabase.table("proposals") \
+ .select("*, campaigns(title), brands(company_name)") \
+ .eq("creator_id", creator_id) \
+ .order("created_at", desc=True) \
+ .range(offset, offset + limit - 1)
+
+ if status:
+ query = query.eq("status", status)
+ if negotiation_status:
+ if negotiation_status == "active":
+ query = query.neq("negotiation_status", "none")
+ else:
+ query = query.eq("negotiation_status", negotiation_status)
+
+ result = query.execute()
+
+ proposals = []
+ for prop in (result.data or []):
+ brand_name = None
+ if isinstance(prop.get("brands"), dict):
+ brand_name = prop["brands"].get("company_name", "Unknown Brand")
+
+ proposal = normalize_proposal_record(
+ prop,
+ brand_name=brand_name,
+ creator_name=prop.get("creator_name")
+ )
+
+ if isinstance(prop.get("campaigns"), dict):
+ proposal["campaign_title"] = prop["campaigns"].get("title")
+ if brand_name:
+ proposal["brand_name"] = brand_name
+
+ proposals.append(proposal)
+
+ return proposals
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching proposals: {str(e)}"
+ ) from e
+
+
+@router.put("/proposals/{proposal_id}/status", response_model=ProposalResponse)
+async def update_proposal_status(
+ proposal_id: str,
+ status_update: ProposalStatusUpdate,
+ user: dict = Depends(get_current_user)
+):
+ """Update proposal status. Can be called by either brand (to withdraw) or creator (to accept/decline)."""
+ supabase = supabase_anon
+
+ try:
+ # Get proposal
+ prop_resp = supabase.table("proposals") \
+ .select("*") \
+ .eq("id", proposal_id) \
+ .single() \
+ .execute()
+
+ if not prop_resp.data:
+ raise HTTPException(status_code=404, detail="Proposal not found")
+
+ proposal = prop_resp.data
+ user_id = user['id']
+
+ # Fetch brand to get user_id
+ brand_resp = supabase.table("brands") \
+ .select("id, user_id, company_name") \
+ .eq("id", proposal["brand_id"]) \
+ .single() \
+ .execute()
+ brand_user_id = brand_resp.data.get("user_id") if brand_resp.data else None
+ brand_data = brand_resp.data if brand_resp.data else None
+
+ # Fetch creator to get user_id
+ creator_resp = supabase.table("creators") \
+ .select("id, user_id, display_name") \
+ .eq("id", proposal["creator_id"]) \
+ .single() \
+ .execute()
+ creator_user_id = creator_resp.data.get("user_id") if creator_resp.data else None
+ creator_data = creator_resp.data if creator_resp.data else None
+
+ # Permission check
+ can_update = False
+ if status_update.status == "withdrawn" and brand_user_id == user_id:
+ can_update = True
+ elif status_update.status in ["accepted", "declined"] and creator_user_id == user_id:
+ can_update = True
+
+ if not can_update:
+ raise HTTPException(
+ status_code=403,
+ detail="You don't have permission to update this proposal"
+ )
+
+ # Update only status - Supabase has serialization issues with returning data
+ # We'll update without expecting a response, and handle the serialization error
+ update_data = {
+ "status": status_update.status
+ }
+
+ # Update the proposal - Supabase will try to return the row but may fail to serialize
+ # Code 200 with "JSON could not be generated" means the update succeeded
+ try:
+ # Update without any select - Supabase will still try to return data by default
+ supabase.table("proposals") \
+ .update(update_data) \
+ .eq("id", proposal_id) \
+ .execute()
+ # If we get here without exception, update succeeded
+ except APIError as api_error:
+ # Check if this is a serialization error (code 200 = success but can't serialize)
+ error_dict = api_error.args[0] if api_error.args else {}
+ error_code = error_dict.get('code', 0)
+ error_msg = str(error_dict.get('message', ''))
+
+ # Code 200 means the update succeeded, but Supabase can't serialize the response
+ if error_code == 200:
+ # Update succeeded - the error is just about serialization
+ # We'll continue and build response from original data
+ pass
+ else:
+ # Real error (not code 200) - this is a failure
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to update proposal: {error_msg}"
+ )
+ except Exception as update_error:
+ # Other unexpected errors
+ error_msg = str(update_error)
+ # Check if it's the serialization error in string form
+ if "code': 200" in error_msg or "JSON could not be generated" in error_msg:
+ # Still a serialization error - update likely succeeded
+ pass
+ else:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to update proposal: {error_msg}"
+ )
+
+ # Fetch updated proposal for clean response using safe helper
+ refreshed = fetch_proposal_with_joins(proposal_id)
+
+ brand_name = None
+ creator_name = None
+ if isinstance(refreshed.get("brands"), dict):
+ brand_name = refreshed["brands"].get("company_name", "Unknown Brand")
+ elif brand_data:
+ brand_name = brand_data.get("company_name", "Unknown Brand")
+
+ if isinstance(refreshed.get("creators"), dict):
+ creator_name = refreshed["creators"].get("display_name")
+ elif creator_data:
+ creator_name = creator_data.get("display_name")
+
+ normalized = normalize_proposal_record(
+ refreshed,
+ brand_name=brand_name,
+ creator_name=creator_name
+ )
+
+ if isinstance(refreshed.get("campaigns"), dict):
+ normalized["campaign_title"] = refreshed["campaigns"].get("title")
+
+ normalized["status"] = status_update.status
+
+ return ProposalResponse.model_validate(normalized)
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ import traceback
+ print(f"Error updating proposal {proposal_id}: {str(e)}")
+ print(traceback.format_exc())
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error updating proposal: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/proposals/{proposal_id}/negotiation/start",
+ response_model=ProposalResponse,
+ status_code=201
+)
+async def start_negotiation(
+ proposal_id: str,
+ payload: NegotiationStartRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """Creator starts a negotiation on a proposal."""
+ supabase = supabase_anon
+
+ if not payload.initial_message and not payload.proposed_terms:
+ raise HTTPException(
+ status_code=400,
+ detail="Provide a message or proposed terms to start negotiation"
+ )
+
+ proposal = fetch_proposal_by_id(proposal_id)
+ # Patch: sanitize content_ideas before any further use
+ proposal["content_ideas"] = sanitize_content_ideas(proposal.get("content_ideas"))
+
+ if proposal["creator_id"] != creator["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have access to this proposal"
+ )
+
+ negotiation_status = proposal.get("negotiation_status", "none")
+ if negotiation_status == "open":
+ raise HTTPException(
+ status_code=400,
+ detail="Negotiation already in progress for this proposal"
+ )
+ if negotiation_status == "finalized":
+ raise HTTPException(
+ status_code=400,
+ detail="Negotiation already finalized for this proposal"
+ )
+
+ thread = normalize_negotiation_thread(proposal.get("negotiation_thread"))
+
+ if payload.initial_message:
+ thread.append(
+ build_thread_entry(
+ sender_id=creator["id"],
+ sender_role="Creator",
+ message=payload.initial_message,
+ entry_type="message"
+ )
+ )
+
+ if payload.proposed_terms:
+ message_text = payload.initial_message or "Creator proposed updated terms."
+ thread.append(
+ build_thread_entry(
+ sender_id=creator["id"],
+ sender_role="Creator",
+ message=message_text,
+ entry_type="terms_proposal",
+ meta={"terms": payload.proposed_terms}
+ )
+ )
+
+ update_data = {
+ "negotiation_status": "open",
+ "negotiation_thread": thread,
+ }
+
+ if payload.proposed_terms:
+ update_data["current_terms"] = payload.proposed_terms
+ current_version = proposal.get("version") or 1
+ update_data["version"] = current_version + 1
+
+ try:
+ supabase.table("proposals") \
+ .update(update_data) \
+ .eq("id", proposal_id) \
+ .execute()
+ except APIError as api_error:
+ # Handle serialization error - update might have succeeded
+ error_dict = api_error.args[0] if api_error.args and isinstance(api_error.args[0], dict) else {}
+ error_code = error_dict.get('code', 0)
+ if error_code != 200:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to start negotiation: {str(api_error)}"
+ ) from api_error
+ # Code 200 means update succeeded but can't serialize response
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to start negotiation: {str(e)}"
+ ) from e
+
+ # Fetch updated proposal using the safe helper function
+ refreshed = fetch_proposal_with_joins(proposal_id)
+
+ brand_name = None
+ if isinstance(refreshed.get("brands"), dict):
+ brand_name = refreshed["brands"].get("company_name", "Unknown Brand")
+
+ normalized = normalize_proposal_record(
+ refreshed,
+ brand_name=brand_name,
+ creator_name=creator.get("display_name")
+ )
+
+ if isinstance(refreshed.get("campaigns"), dict):
+ normalized["campaign_title"] = refreshed["campaigns"].get("title")
+
+ normalized["negotiation_status"] = "open"
+
+ return ProposalResponse.model_validate(normalized)
+
+
+@router.post(
+ "/proposals/{proposal_id}/negotiation/messages",
+ response_model=ProposalResponse
+)
+async def post_negotiation_message(
+ proposal_id: str,
+ payload: NegotiationMessageRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Post a new message to the negotiation thread."""
+ supabase = supabase_anon
+ proposal = fetch_proposal_by_id(proposal_id)
+
+ if proposal.get("negotiation_status") != "open":
+ raise HTTPException(
+ status_code=400,
+ detail="Negotiation is not active for this proposal"
+ )
+
+ sender_role = user.get("role")
+ sender_id = None
+ brand_profile = None
+ creator_profile = None
+
+ if sender_role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != proposal["brand_id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have permission to message on this negotiation"
+ )
+ sender_id = brand_profile["id"]
+ sender_role_label = "Brand"
+ elif sender_role == "Creator":
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != proposal["creator_id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have permission to message on this negotiation"
+ )
+ sender_id = creator_profile["id"]
+ sender_role_label = "Creator"
+ else:
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can participate in negotiations"
+ )
+
+ thread = normalize_negotiation_thread(proposal.get("negotiation_thread"))
+ thread.append(
+ build_thread_entry(
+ sender_id=sender_id,
+ sender_role=sender_role_label,
+ message=payload.message,
+ entry_type="message"
+ )
+ )
+
+ try:
+ supabase.table("proposals") \
+ .update({"negotiation_thread": thread}) \
+ .eq("id", proposal_id) \
+ .execute()
+ except APIError as api_error:
+ # Handle serialization error - update might have succeeded
+ error_dict = api_error.args[0] if api_error.args and isinstance(api_error.args[0], dict) else {}
+ error_code = error_dict.get('code', 0)
+ if error_code != 200:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to post message: {str(api_error)}"
+ ) from api_error
+ # Code 200 means update succeeded but can't serialize response
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to post message: {str(e)}"
+ ) from e
+
+ # Fetch updated proposal using the safe helper function
+ refreshed = fetch_proposal_with_joins(proposal_id)
+
+ brand_name = None
+ creator_name = None
+ if isinstance(refreshed.get("brands"), dict):
+ brand_name = refreshed["brands"].get("company_name", "Unknown Brand")
+ elif brand_profile:
+ brand_name = brand_profile.get("company_name")
+
+ if isinstance(refreshed.get("creators"), dict):
+ creator_name = refreshed["creators"].get("display_name")
+ elif creator_profile:
+ creator_name = creator_profile.get("display_name")
+
+ normalized = normalize_proposal_record(
+ refreshed,
+ brand_name=brand_name,
+ creator_name=creator_name
+ )
+
+ if isinstance(refreshed.get("campaigns"), dict):
+ normalized["campaign_title"] = refreshed["campaigns"].get("title")
+
+ return ProposalResponse.model_validate(normalized)
+
+
+@router.put(
+ "/proposals/{proposal_id}/negotiation/terms",
+ response_model=ProposalResponse
+)
+async def update_negotiation_terms(
+ proposal_id: str,
+ payload: NegotiationTermsUpdateRequest,
+ brand: dict = Depends(get_current_brand)
+):
+ """Brand updates the proposal terms during negotiation."""
+ supabase = supabase_anon
+ proposal = fetch_proposal_by_id(proposal_id)
+
+ if proposal["brand_id"] != brand["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have access to this proposal"
+ )
+
+ if proposal.get("negotiation_status") != "open":
+ raise HTTPException(
+ status_code=400,
+ detail="Negotiation is not active for this proposal"
+ )
+
+ if not payload.terms:
+ raise HTTPException(
+ status_code=400,
+ detail="Updated terms cannot be empty"
+ )
+
+ thread = normalize_negotiation_thread(proposal.get("negotiation_thread"))
+ note_message = payload.note or "Brand updated the proposal terms."
+ thread.append(
+ build_thread_entry(
+ sender_id=brand["id"],
+ sender_role="Brand",
+ message=note_message,
+ entry_type="terms_update",
+ meta={"terms": payload.terms}
+ )
+ )
+
+ update_data = {
+ "current_terms": payload.terms,
+ "version": (proposal.get("version") or 1) + 1,
+ "negotiation_thread": thread,
+ }
+
+ try:
+ supabase.table("proposals") \
+ .update(update_data) \
+ .eq("id", proposal_id) \
+ .execute()
+ except APIError as api_error:
+ # Handle serialization error - update might have succeeded
+ error_dict = api_error.args[0] if api_error.args and isinstance(api_error.args[0], dict) else {}
+ error_code = error_dict.get('code', 0)
+ if error_code != 200:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to update negotiation terms: {str(api_error)}"
+ ) from api_error
+ # Code 200 means update succeeded but can't serialize response
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to update negotiation terms: {str(e)}"
+ ) from e
+
+ # Fetch updated proposal using the safe helper function
+ refreshed = fetch_proposal_with_joins(proposal_id)
+
+ brand_name = None
+ creator_name = None
+ if isinstance(refreshed.get("brands"), dict):
+ brand_name = refreshed["brands"].get("company_name", "Unknown Brand")
+ else:
+ brand_name = brand.get("company_name")
+
+ if isinstance(refreshed.get("creators"), dict):
+ creator_name = refreshed["creators"].get("display_name")
+
+ normalized = normalize_proposal_record(
+ refreshed,
+ brand_name=brand_name,
+ creator_name=creator_name
+ )
+
+ if isinstance(refreshed.get("campaigns"), dict):
+ normalized["campaign_title"] = refreshed["campaigns"].get("title")
+
+ return ProposalResponse.model_validate(normalized)
+
+
+@router.post(
+ "/proposals/{proposal_id}/negotiation/accept",
+ response_model=AcceptNegotiationResponse
+)
+async def accept_negotiation_terms(
+ proposal_id: str,
+ payload: AcceptNegotiationRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """Creator accepts the latest negotiation terms to finalize the deal."""
+ supabase = supabase_anon
+ proposal = fetch_proposal_by_id(proposal_id)
+
+ if proposal["creator_id"] != creator["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have access to this proposal"
+ )
+
+ if proposal.get("negotiation_status") != "open":
+ raise HTTPException(
+ status_code=400,
+ detail="Negotiation is not active for this proposal"
+ )
+
+ current_terms = normalize_current_terms(proposal.get("current_terms"))
+ if not current_terms:
+ raise HTTPException(
+ status_code=400,
+ detail="No terms available to accept"
+ )
+
+ thread = normalize_negotiation_thread(proposal.get("negotiation_thread"))
+
+ if payload.message:
+ thread.append(
+ build_thread_entry(
+ sender_id=creator["id"],
+ sender_role="Creator",
+ message=payload.message,
+ entry_type="message"
+ )
+ )
+
+ thread.append(
+ build_thread_entry(
+ sender_id=creator["id"],
+ sender_role="Creator",
+ message="Creator accepted the latest terms.",
+ entry_type="acceptance",
+ meta={"terms": current_terms}
+ )
+ )
+
+ contract_payload = {
+ "proposal_id": proposal["id"],
+ "brand_id": proposal["brand_id"],
+ "creator_id": proposal["creator_id"],
+ "terms": current_terms,
+ "status": "awaiting_signature"
+ }
+
+ try:
+ contract_resp = supabase.table("contracts") \
+ .insert(contract_payload) \
+ .execute()
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to create contract: {str(e)}"
+ ) from e
+
+ if not contract_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to create contract record"
+ )
+
+ contract_record = contract_resp.data[0]
+
+ # Copy deliverables from campaign_deliverables to contract_deliverables
+ try:
+ campaign_id = proposal.get("campaign_id")
+ if campaign_id:
+ # Fetch all deliverables for the campaign
+ campaign_deliverables_resp = supabase.table("campaign_deliverables") \
+ .select("*") \
+ .eq("campaign_id", campaign_id) \
+ .execute()
+
+ campaign_deliverables = campaign_deliverables_resp.data or []
+
+ if campaign_deliverables:
+ # Create contract deliverables from campaign deliverables
+ contract_deliverables_data = []
+ for camp_deliv in campaign_deliverables:
+ # Build description from campaign deliverable data
+ description_parts = []
+ if camp_deliv.get("content_type"):
+ description_parts.append(camp_deliv["content_type"])
+ if camp_deliv.get("platform"):
+ description_parts.append(f"on {camp_deliv['platform']}")
+ if camp_deliv.get("quantity") and camp_deliv["quantity"] > 1:
+ description_parts.append(f"({camp_deliv['quantity']} items)")
+ if camp_deliv.get("guidance"):
+ description_parts.append(f"- {camp_deliv['guidance']}")
+
+ description = " ".join(description_parts) if description_parts else "Deliverable"
+
+ contract_deliverable = {
+ "contract_id": contract_record["id"],
+ "campaign_deliverable_id": camp_deliv["id"],
+ "description": description,
+ "status": "pending",
+ "brand_approval": False,
+ "creator_approval": False,
+ }
+ contract_deliverables_data.append(contract_deliverable)
+
+ # Insert all contract deliverables
+ if contract_deliverables_data:
+ supabase.table("contract_deliverables") \
+ .insert(contract_deliverables_data) \
+ .execute()
+ except Exception as e:
+ # Log the error but don't fail the contract creation
+ # This ensures contract creation succeeds even if deliverable copying fails
+ import logging
+ logger = logging.getLogger(__name__)
+ logger.warning(f"Failed to copy deliverables to contract: {str(e)}")
+
+ update_data = {
+ "negotiation_status": "finalized",
+ "status": "accepted",
+ "contract_id": contract_record["id"],
+ "negotiation_thread": thread,
+ }
+
+ try:
+ supabase.table("proposals") \
+ .update(update_data) \
+ .eq("id", proposal_id) \
+ .execute()
+ except APIError as api_error:
+ # Handle serialization error - update might have succeeded
+ error_dict = api_error.args[0] if api_error.args and isinstance(api_error.args[0], dict) else {}
+ error_code = error_dict.get('code', 0)
+ if error_code != 200:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to finalize negotiation: {str(api_error)}"
+ ) from api_error
+ # Code 200 means update succeeded but can't serialize response
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to finalize negotiation: {str(e)}"
+ ) from e
+
+ # Fetch updated proposal using the safe helper function
+ refreshed_proposal = fetch_proposal_with_joins(proposal_id)
+
+ # Fetch contract - use explicit fields to avoid serialization issues
+ try:
+ refreshed_contract_resp = supabase.table("contracts") \
+ .select("id,proposal_id,brand_id,creator_id,terms,status,created_at,updated_at") \
+ .eq("id", contract_record["id"]) \
+ .single() \
+ .execute()
+ refreshed_contract = refreshed_contract_resp.data if refreshed_contract_resp and refreshed_contract_resp.data else None
+ except APIError as api_error:
+ # Handle serialization error
+ error_dict = api_error.args[0] if api_error.args and isinstance(api_error.args[0], dict) else {}
+ error_code = error_dict.get('code', 0)
+ if error_code == 200:
+ # Try to fetch without joins
+ refreshed_contract_resp = supabase.table("contracts") \
+ .select("id,proposal_id,brand_id,creator_id,terms,status,created_at,updated_at") \
+ .eq("id", contract_record["id"]) \
+ .single() \
+ .execute()
+ refreshed_contract = refreshed_contract_resp.data if refreshed_contract_resp and refreshed_contract_resp.data else None
+ else:
+ raise HTTPException(status_code=500, detail=f"Failed to fetch contract: {str(api_error)}") from api_error
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Failed to fetch contract: {str(e)}") from e
+
+ if not refreshed_contract:
+ raise HTTPException(status_code=500, detail="Failed to fetch contract data")
+
+ # Add proposal data from the already fetched proposal
+ refreshed_contract["proposals"] = refreshed_proposal
+
+ brand_name = None
+ creator_name = None
+ if isinstance(refreshed_proposal.get("brands"), dict):
+ brand_name = refreshed_proposal["brands"].get("company_name", "Unknown Brand")
+ if isinstance(refreshed_proposal.get("creators"), dict):
+ creator_name = refreshed_proposal["creators"].get("display_name")
+
+ normalized_proposal = normalize_proposal_record(
+ refreshed_proposal,
+ brand_name=brand_name,
+ creator_name=creator_name
+ )
+ if isinstance(refreshed_proposal.get("campaigns"), dict):
+ normalized_proposal["campaign_title"] = refreshed_proposal["campaigns"].get("title")
+
+ normalized_contract = normalize_contract_record(refreshed_contract)
+ if brand_name and not normalized_contract.get("brand_name"):
+ normalized_contract["brand_name"] = brand_name
+ if creator_name and not normalized_contract.get("creator_name"):
+ normalized_contract["creator_name"] = creator_name
+
+ if normalized_contract.get("proposal"):
+ # Ensure nested proposal includes enriched fields
+ proposal_instance = ProposalResponse.model_validate(
+ normalized_proposal
+ )
+ normalized_contract["proposal"] = proposal_instance
+ normalized_contract["negotiation_thread"] = proposal_instance.negotiation_thread
+
+ return AcceptNegotiationResponse(
+ proposal=ProposalResponse.model_validate(normalized_proposal),
+ contract=ContractResponse.model_validate(normalized_contract)
+ )
+
+
+@router.get(
+ "/proposals/negotiations",
+ response_model=List[ProposalResponse]
+)
+async def list_negotiations(
+ status: Optional[str] = Query(None, description="Filter by negotiation status"),
+ user: dict = Depends(get_current_user)
+):
+ """Fetch proposals for the current user filtered by negotiation status."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can view negotiations"
+ )
+
+ brand_profile = None
+ creator_profile = None
+
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile:
+ raise HTTPException(
+ status_code=404,
+ detail="Brand profile not found"
+ )
+ query = supabase.table("proposals") \
+ .select("*, campaigns(title), brands(company_name), creators(display_name)") \
+ .eq("brand_id", brand_profile["id"]) \
+ .order("updated_at", desc=True)
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile:
+ raise HTTPException(
+ status_code=404,
+ detail="Creator profile not found"
+ )
+ query = supabase.table("proposals") \
+ .select("*, campaigns(title), brands(company_name), creators(display_name)") \
+ .eq("creator_id", creator_profile["id"]) \
+ .order("updated_at", desc=True)
+
+ if status:
+ query = query.eq("negotiation_status", status)
+ else:
+ query = query.neq("negotiation_status", "none")
+
+ response = query.execute()
+
+ negotiations = []
+ for prop in (response.data or []):
+ brand_name = None
+ creator_name = None
+ if isinstance(prop.get("brands"), dict):
+ brand_name = prop["brands"].get("company_name", "Unknown Brand")
+ if isinstance(prop.get("creators"), dict):
+ creator_name = prop["creators"].get("display_name")
+
+ normalized = normalize_proposal_record(
+ prop,
+ brand_name=brand_name,
+ creator_name=creator_name
+ )
+
+ if isinstance(prop.get("campaigns"), dict):
+ normalized["campaign_title"] = prop["campaigns"].get("title")
+
+ negotiations.append(normalized)
+
+ return [ProposalResponse.model_validate(item) for item in negotiations]
+
+
+def _normalize_contract_response(record: dict) -> ContractResponse:
+ """Helper to normalize and build ContractResponse model."""
+ normalized = normalize_contract_record(record)
+
+ proposal_payload = normalized.get("proposal")
+ proposal_model = None
+ if proposal_payload:
+ proposal_model = ProposalResponse.model_validate(proposal_payload)
+ normalized["proposal"] = proposal_model
+ normalized["negotiation_thread"] = proposal_model.negotiation_thread
+ else:
+ normalized["proposal"] = None
+ normalized["negotiation_thread"] = None
+
+ # Include contract link fields and tracking
+ normalized["unsigned_contract_link"] = record.get("unsigned_contract_link")
+ normalized["signed_contract_link"] = record.get("signed_contract_link")
+ normalized["unsigned_contract_downloaded_by_creator"] = record.get("unsigned_contract_downloaded_by_creator", False)
+ normalized["signed_contract_downloaded_by_brand"] = record.get("signed_contract_downloaded_by_brand", False)
+ normalized["pending_status_change"] = normalize_json_field(record.get("pending_status_change"), None)
+
+ return ContractResponse.model_validate(normalized)
+
+
+@router.get(
+ "/contracts",
+ response_model=List[ContractResponse]
+)
+async def list_contracts(
+ status: Optional[str] = Query(None, description="Filter contracts by status"),
+ user: dict = Depends(get_current_user)
+):
+ """Fetch contracts for the authenticated brand or creator."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can view contracts"
+ )
+
+ brand_profile = None
+ creator_profile = None
+
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile:
+ raise HTTPException(status_code=404, detail="Brand profile not found")
+ query = supabase.table("contracts") \
+ .select("*, proposals(*, campaigns(title), brands(company_name), creators(display_name))") \
+ .eq("brand_id", brand_profile["id"]) \
+ .order("updated_at", desc=True)
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile:
+ raise HTTPException(status_code=404, detail="Creator profile not found")
+ query = supabase.table("contracts") \
+ .select("*, proposals(*, campaigns(title), brands(company_name), creators(display_name))") \
+ .eq("creator_id", creator_profile["id"]) \
+ .order("updated_at", desc=True)
+
+ if status:
+ query = query.eq("status", status)
+
+ try:
+ response = query.execute()
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to fetch contracts: {str(e)}"
+ ) from e
+
+ records = response.data or []
+ return [_normalize_contract_response(record) for record in records]
+
+
+@router.get(
+ "/contracts/{contract_id}",
+ response_model=ContractResponse
+)
+async def get_contract_detail(
+ contract_id: str,
+ user: dict = Depends(get_current_user)
+):
+ """Fetch a single contract with negotiation history."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can view contracts"
+ )
+
+ try:
+ record_resp = supabase.table("contracts") \
+ .select("*, proposals(*, campaigns(title), brands(company_name), creators(display_name))") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to fetch contract: {str(e)}"
+ ) from e
+
+ record = record_resp.data if record_resp and record_resp.data else None
+ if not record:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ brand_id = record.get("brand_id")
+ creator_id = record.get("creator_id")
+
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != brand_id:
+ raise HTTPException(status_code=403, detail="Access denied for this contract")
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != creator_id:
+ raise HTTPException(status_code=403, detail="Access denied for this contract")
+
+ return _normalize_contract_response(record)
+
+
+class ContractLinkUpdate(BaseModel):
+ """Schema for updating contract link."""
+ link: str = Field(..., min_length=1, description="Cloud storage link (Google Drive, Dropbox, etc.)")
+
+
+@router.put(
+ "/contracts/{contract_id}/unsigned-link",
+ response_model=ContractResponse
+)
+async def update_unsigned_contract_link(
+ contract_id: str,
+ payload: ContractLinkUpdate,
+ brand: dict = Depends(get_current_brand)
+):
+ """Brand uploads a link to the unsigned contract."""
+ supabase = supabase_anon
+
+ try:
+ # Verify contract exists and belongs to this brand
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ if contract_resp.data["brand_id"] != brand["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have permission to update this contract"
+ )
+
+ # Update the unsigned contract link
+ update_resp = supabase.table("contracts") \
+ .update({"unsigned_contract_link": payload.link}) \
+ .eq("id", contract_id) \
+ .execute()
+
+ if not update_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to update contract link"
+ )
+
+ # Fetch updated contract
+ updated_contract_resp = supabase.table("contracts") \
+ .select("*, proposals(*, campaigns(title), brands(company_name), creators(display_name))") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not updated_contract_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to fetch updated contract"
+ )
+
+ return _normalize_contract_response(updated_contract_resp.data)
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error updating contract link: {str(e)}"
+ ) from e
+
+
+@router.put(
+ "/contracts/{contract_id}/signed-link",
+ response_model=ContractResponse
+)
+async def update_signed_contract_link(
+ contract_id: str,
+ payload: ContractLinkUpdate,
+ creator: dict = Depends(get_current_creator)
+):
+ """Creator uploads a link to the signed contract."""
+ supabase = supabase_anon
+
+ try:
+ # Verify contract exists and belongs to this creator
+ contract_resp = supabase.table("contracts") \
+ .select("id, creator_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ if contract_resp.data["creator_id"] != creator["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have permission to update this contract"
+ )
+
+ contract = contract_resp.data
+ creator_profile = fetch_creator_profile_by_user_id(creator["id"])
+
+ # Update the signed contract link
+ update_resp = supabase.table("contracts") \
+ .update({"signed_contract_link": payload.link}) \
+ .eq("id", contract_id) \
+ .execute()
+
+ if not update_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to update contract link"
+ )
+
+ # Check if this is the initial contract finalization (both parties have signed)
+ # Brand uploaded unsigned (signed it), Creator now uploads signed (signed it)
+ # Create initial version if no versions exist
+ versions_check = supabase.table("contract_versions") \
+ .select("id") \
+ .eq("contract_id", contract_id) \
+ .limit(1) \
+ .execute()
+
+ if not versions_check.data:
+ # Create initial version - both parties have signed
+ version_data = {
+ "contract_id": contract_id,
+ "version_number": 1,
+ "file_url": payload.link,
+ "uploaded_by": creator_profile["id"] if creator_profile else None,
+ "status": "final",
+ "brand_approval": True, # Brand already signed (uploaded unsigned)
+ "creator_approval": True, # Creator just signed (uploaded signed)
+ "change_reason": "Initial signed contract",
+ "is_current": True,
+ }
+
+ version_insert = supabase.table("contract_versions") \
+ .insert(version_data) \
+ .execute()
+
+ if version_insert.data:
+ # Update contract's current_version_id
+ supabase.table("contracts") \
+ .update({"current_version_id": version_insert.data[0]["id"]}) \
+ .eq("id", contract_id) \
+ .execute()
+
+ # Fetch updated contract
+ updated_contract_resp = supabase.table("contracts") \
+ .select("*, proposals(*, campaigns(title), brands(company_name), creators(display_name))") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not updated_contract_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to fetch updated contract"
+ )
+
+ return _normalize_contract_response(updated_contract_resp.data)
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error updating contract link: {str(e)}"
+ ) from e
+
+
+class ContractChatMessage(BaseModel):
+ """Schema for contract chat message."""
+ id: str
+ contract_id: str
+ sender_id: str
+ sender_role: str
+ message: str
+ created_at: datetime
+
+
+class ContractChatMessageCreate(BaseModel):
+ """Schema for creating a new contract chat message."""
+ message: str = Field(..., min_length=1, max_length=5000)
+
+
+class ContractStatusChangeRequest(BaseModel):
+ """Schema for requesting a contract status change."""
+ new_status: str = Field(
+ ...,
+ pattern="^(signed_and_active|paused|completed_successfully|terminated)$",
+ description="New contract status"
+ )
+
+
+@router.post(
+ "/contracts/{contract_id}/track-unsigned-download",
+ response_model=ContractResponse
+)
+async def track_unsigned_contract_download(
+ contract_id: str,
+ creator: dict = Depends(get_current_creator)
+):
+ """Track when Creator downloads the unsigned contract."""
+ supabase = supabase_anon
+
+ try:
+ # Verify contract exists and belongs to this creator
+ contract_resp = supabase.table("contracts") \
+ .select("id, creator_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ if contract_resp.data["creator_id"] != creator["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have permission to track this download"
+ )
+
+ # Update the download tracking
+ update_resp = supabase.table("contracts") \
+ .update({"unsigned_contract_downloaded_by_creator": True}) \
+ .eq("id", contract_id) \
+ .execute()
+
+ if not update_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to track download"
+ )
+
+ # Fetch updated contract
+ updated_contract_resp = supabase.table("contracts") \
+ .select("*, proposals(*, campaigns(title), brands(company_name), creators(display_name))") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not updated_contract_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to fetch updated contract"
+ )
+
+ return _normalize_contract_response(updated_contract_resp.data)
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error tracking download: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/track-signed-download",
+ response_model=ContractResponse
+)
+async def track_signed_contract_download(
+ contract_id: str,
+ brand: dict = Depends(get_current_brand)
+):
+ """Track when Brand downloads the signed contract."""
+ supabase = supabase_anon
+
+ try:
+ # Verify contract exists and belongs to this brand
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id, signed_contract_link, signed_contract_downloaded_by_brand") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ if contract_resp.data["brand_id"] != brand["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have permission to track this download"
+ )
+
+ contract = contract_resp.data
+ brand_profile = fetch_brand_profile_by_user_id(brand["id"])
+
+ # Update the download tracking
+ update_resp = supabase.table("contracts") \
+ .update({"signed_contract_downloaded_by_brand": True}) \
+ .eq("id", contract_id) \
+ .execute()
+
+ if not update_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to track download"
+ )
+
+
+ # Fetch updated contract
+ updated_contract_resp = supabase.table("contracts") \
+ .select("*, proposals(*, campaigns(title), brands(company_name), creators(display_name))") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not updated_contract_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to fetch updated contract"
+ )
+
+ return _normalize_contract_response(updated_contract_resp.data)
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error tracking download: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/request-status-change",
+ response_model=ContractResponse
+)
+async def request_contract_status_change(
+ contract_id: str,
+ payload: ContractStatusChangeRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Request a contract status change. Requires approval from the other party."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can request status changes"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id, unsigned_contract_link, signed_contract_link, unsigned_contract_downloaded_by_creator, signed_contract_downloaded_by_brand") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+
+ # Verify user has access to this contract
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ # Check if all workflow steps are completed
+ workflow_complete = (
+ contract.get("unsigned_contract_link") and
+ contract.get("unsigned_contract_downloaded_by_creator") and
+ contract.get("signed_contract_link") and
+ contract.get("signed_contract_downloaded_by_brand")
+ )
+
+ if not workflow_complete:
+ raise HTTPException(
+ status_code=400,
+ detail="All contract file exchange steps must be completed before requesting status changes"
+ )
+
+ # Check if there's already a pending request
+ pending = contract.get("pending_status_change")
+ if pending:
+ raise HTTPException(
+ status_code=400,
+ detail="There is already a pending status change request"
+ )
+
+ # Create pending status change request
+ pending_request = {
+ "requested_status": payload.new_status,
+ "requesting_party": role,
+ "requested_at": datetime.now(timezone.utc).isoformat()
+ }
+
+ update_resp = supabase.table("contracts") \
+ .update({"pending_status_change": pending_request}) \
+ .eq("id", contract_id) \
+ .execute()
+
+ if not update_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to create status change request"
+ )
+
+ # Fetch updated contract
+ updated_contract_resp = supabase.table("contracts") \
+ .select("*, proposals(*, campaigns(title), brands(company_name), creators(display_name))") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not updated_contract_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to fetch updated contract"
+ )
+
+ return _normalize_contract_response(updated_contract_resp.data)
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error requesting status change: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/respond-status-change",
+ response_model=ContractResponse
+)
+async def respond_to_status_change_request(
+ contract_id: str,
+ approved: bool = Query(..., description="Whether to approve the status change"),
+ user: dict = Depends(get_current_user)
+):
+ """Approve or deny a pending status change request."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can respond to status change requests"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id, pending_status_change") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+
+ # Verify user has access to this contract
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ pending = contract.get("pending_status_change")
+ if not pending or not isinstance(pending, dict):
+ raise HTTPException(
+ status_code=400,
+ detail="No pending status change request found"
+ )
+
+ requesting_party = pending.get("requesting_party")
+ if requesting_party == role:
+ raise HTTPException(
+ status_code=400,
+ detail="You cannot respond to your own status change request"
+ )
+
+ if approved:
+ # Update contract status and clear pending request
+ new_status = pending.get("requested_status")
+ update_resp = supabase.table("contracts") \
+ .update({
+ "status": new_status,
+ "pending_status_change": None
+ }) \
+ .eq("id", contract_id) \
+ .execute()
+ else:
+ # Just clear the pending request
+ update_resp = supabase.table("contracts") \
+ .update({"pending_status_change": None}) \
+ .eq("id", contract_id) \
+ .execute()
+
+ if not update_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to update contract"
+ )
+
+ # Fetch updated contract
+ updated_contract_resp = supabase.table("contracts") \
+ .select("*, proposals(*, campaigns(title), brands(company_name), creators(display_name))") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not updated_contract_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to fetch updated contract"
+ )
+
+ return _normalize_contract_response(updated_contract_resp.data)
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error responding to status change: {str(e)}"
+ ) from e
+
+
+@router.get(
+ "/contracts/{contract_id}/chat",
+ response_model=List[ContractChatMessage]
+)
+async def get_contract_chat_messages(
+ contract_id: str,
+ user: dict = Depends(get_current_user)
+):
+ """Fetch all chat messages for a contract."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can view contract chats"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+
+ # Verify user has access to this contract
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ # Fetch chat messages
+ messages_resp = supabase.table("contract_chats") \
+ .select("*") \
+ .eq("contract_id", contract_id) \
+ .order("created_at", desc=False) \
+ .execute()
+
+ messages = []
+ for msg in (messages_resp.data or []):
+ # Parse datetime
+ created_at = parse_datetime(msg.get("created_at"))
+ messages.append({
+ "id": msg.get("id"),
+ "contract_id": msg.get("contract_id"),
+ "sender_id": msg.get("sender_id"),
+ "sender_role": msg.get("sender_role", "").capitalize(), # Capitalize for display
+ "message": msg.get("message"),
+ "created_at": created_at
+ })
+
+ return [ContractChatMessage.model_validate(msg) for msg in messages]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching chat messages: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/chat",
+ response_model=ContractChatMessage,
+ status_code=201
+)
+async def post_contract_chat_message(
+ contract_id: str,
+ payload: ContractChatMessageCreate,
+ user: dict = Depends(get_current_user)
+):
+ """Post a new message to the contract chat."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can send messages"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+ sender_id = None
+ sender_role_db = None
+
+ # Verify user has access and get sender ID
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ sender_id = brand_profile["id"]
+ sender_role_db = "brand"
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ sender_id = creator_profile["id"]
+ sender_role_db = "creator"
+
+ # Insert chat message
+ message_data = {
+ "contract_id": contract_id,
+ "sender_id": sender_id,
+ "sender_role": sender_role_db,
+ "message": payload.message
+ }
+
+ insert_resp = supabase.table("contract_chats") \
+ .insert(message_data) \
+ .execute()
+
+ if not insert_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to post message"
+ )
+
+ message_record = insert_resp.data[0]
+ created_at = parse_datetime(message_record.get("created_at"))
+ sender_role_db = message_record.get("sender_role", "")
+
+ return ContractChatMessage(
+ id=message_record.get("id"),
+ contract_id=message_record.get("contract_id"),
+ sender_id=message_record.get("sender_id"),
+ sender_role=sender_role_db.capitalize() if sender_role_db else role, # Capitalize for consistency with GET endpoint
+ message=message_record.get("message"),
+ created_at=created_at
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error posting message: {str(e)}"
+ ) from e
+
+
+# ============================================================================
+# DELIVERABLES TRACKING ENDPOINTS
+# ============================================================================
+
+class DeliverableCreate(BaseModel):
+ """Schema for creating a deliverable."""
+ description: str = Field(..., min_length=1, max_length=2000)
+ due_date: Optional[datetime] = None
+
+
+class DeliverableUpdate(BaseModel):
+ """Schema for updating a deliverable."""
+ description: Optional[str] = Field(None, min_length=1, max_length=2000)
+ due_date: Optional[datetime] = None
+
+
+class DeliverableResponse(BaseModel):
+ """Schema for deliverable response."""
+ id: str
+ contract_id: str
+ description: str
+ due_date: Optional[datetime]
+ status: str
+ submission_url: Optional[str]
+ review_comment: Optional[str]
+ rejection_reason: Optional[str]
+ brand_approval: bool
+ creator_approval: bool
+ created_at: datetime
+ updated_at: datetime
+
+
+class DeliverableSubmission(BaseModel):
+ """Schema for submitting a deliverable URL."""
+ submission_url: str = Field(..., min_length=1, max_length=2000)
+
+
+class DeliverableReview(BaseModel):
+ """Schema for reviewing a deliverable."""
+ approved: bool
+ review_comment: Optional[str] = Field(None, max_length=2000)
+ rejection_reason: Optional[str] = Field(None, max_length=2000)
+
+
+class DeliverablesListUpdate(BaseModel):
+ """Schema for updating the deliverables list (add/edit/remove)."""
+ deliverables: List[DeliverableCreate] = Field(..., min_items=0)
+
+
+class ApprovalRequest(BaseModel):
+ """Schema for approving deliverables list."""
+ approved: bool
+
+
+
+
+@router.get(
+ "/contracts/{contract_id}/deliverables",
+ response_model=List[DeliverableResponse]
+)
+async def get_contract_deliverables(
+ contract_id: str,
+ user: dict = Depends(get_current_user)
+):
+ """Fetch all deliverables for a contract."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can view deliverables"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+
+ # Verify user has access to this contract
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ # Fetch deliverables
+ deliverables_resp = supabase.table("contract_deliverables") \
+ .select("*") \
+ .eq("contract_id", contract_id) \
+ .order("created_at", desc=False) \
+ .execute()
+
+ deliverables = []
+ for deliv in (deliverables_resp.data or []):
+ due_date = deliv.get("due_date")
+ deliverables.append({
+ "id": deliv.get("id"),
+ "contract_id": deliv.get("contract_id"),
+ "description": deliv.get("description"),
+ "due_date": parse_datetime(due_date) if due_date else None,
+ "status": deliv.get("status", "pending"),
+ "submission_url": deliv.get("submission_url"),
+ "review_comment": deliv.get("review_comment"),
+ "rejection_reason": deliv.get("rejection_reason"),
+ "brand_approval": deliv.get("brand_approval", False),
+ "creator_approval": deliv.get("creator_approval", False),
+ "created_at": parse_datetime(deliv.get("created_at")),
+ "updated_at": parse_datetime(deliv.get("updated_at")),
+ })
+
+ return [DeliverableResponse.model_validate(d) for d in deliverables]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching deliverables: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/deliverables",
+ response_model=List[DeliverableResponse],
+ status_code=201
+)
+async def create_or_update_deliverables_list(
+ contract_id: str,
+ payload: DeliverablesListUpdate,
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Brand creates or updates the deliverables list.
+ This resets approvals - both parties must re-approve after changes.
+ """
+ supabase = supabase_anon
+
+ try:
+ # Verify contract exists and belongs to this brand
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ if contract_resp.data["brand_id"] != brand["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="Only the brand can create/update deliverables"
+ )
+
+ # Delete existing deliverables
+ supabase.table("contract_deliverables") \
+ .delete() \
+ .eq("contract_id", contract_id) \
+ .execute()
+
+ # Create new deliverables
+ new_deliverables = []
+ for deliv in payload.deliverables:
+ deliverable_data = {
+ "contract_id": contract_id,
+ "description": deliv.description,
+ "due_date": deliv.due_date.isoformat() if deliv.due_date else None,
+ "status": "pending",
+ "brand_approval": False,
+ "creator_approval": False,
+ }
+
+ insert_resp = supabase.table("contract_deliverables") \
+ .insert(deliverable_data) \
+ .execute()
+
+ if insert_resp.data:
+ new_deliverables.append(insert_resp.data[0])
+
+ # Format response
+ formatted = []
+ for deliv in new_deliverables:
+ due_date = deliv.get("due_date")
+ formatted.append({
+ "id": deliv.get("id"),
+ "contract_id": deliv.get("contract_id"),
+ "description": deliv.get("description"),
+ "due_date": parse_datetime(due_date) if due_date else None,
+ "status": deliv.get("status", "pending"),
+ "submission_url": deliv.get("submission_url"),
+ "review_comment": deliv.get("review_comment"),
+ "rejection_reason": deliv.get("rejection_reason"),
+ "brand_approval": deliv.get("brand_approval", False),
+ "creator_approval": deliv.get("creator_approval", False),
+ "created_at": parse_datetime(deliv.get("created_at")),
+ "updated_at": parse_datetime(deliv.get("updated_at")),
+ })
+
+ return [DeliverableResponse.model_validate(d) for d in formatted]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error creating/updating deliverables: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/deliverables/approve",
+ response_model=List[DeliverableResponse]
+)
+async def approve_deliverables_list(
+ contract_id: str,
+ payload: ApprovalRequest,
+ user: dict = Depends(get_current_user)
+):
+ """
+ Brand or Creator approves the deliverables list.
+ Once both parties approve, the list is finalized and locked.
+ """
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can approve deliverables"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+
+ # Verify user has access
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ approval_field = "brand_approval"
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ approval_field = "creator_approval"
+
+ # Update all deliverables for this contract
+ update_data = {approval_field: payload.approved}
+ supabase.table("contract_deliverables") \
+ .update(update_data) \
+ .eq("contract_id", contract_id) \
+ .execute()
+
+ # Fetch updated deliverables
+ deliverables_resp = supabase.table("contract_deliverables") \
+ .select("*") \
+ .eq("contract_id", contract_id) \
+ .order("created_at", desc=False) \
+ .execute()
+
+ deliverables = []
+ for deliv in (deliverables_resp.data or []):
+ due_date = deliv.get("due_date")
+ deliverables.append({
+ "id": deliv.get("id"),
+ "contract_id": deliv.get("contract_id"),
+ "description": deliv.get("description"),
+ "due_date": parse_datetime(due_date) if due_date else None,
+ "status": deliv.get("status", "pending"),
+ "submission_url": deliv.get("submission_url"),
+ "review_comment": deliv.get("review_comment"),
+ "rejection_reason": deliv.get("rejection_reason"),
+ "brand_approval": deliv.get("brand_approval", False),
+ "creator_approval": deliv.get("creator_approval", False),
+ "created_at": parse_datetime(deliv.get("created_at")),
+ "updated_at": parse_datetime(deliv.get("updated_at")),
+ })
+
+ return [DeliverableResponse.model_validate(d) for d in deliverables]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error approving deliverables: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/deliverables/{deliverable_id}/submit",
+ response_model=DeliverableResponse
+)
+async def submit_deliverable(
+ contract_id: str,
+ deliverable_id: str,
+ payload: DeliverableSubmission,
+ creator: dict = Depends(get_current_creator)
+):
+ """
+ Creator submits a URL for a deliverable.
+ Only allowed if the deliverables list is approved by both parties.
+ """
+ supabase = supabase_anon
+
+ try:
+ # Verify contract exists and belongs to this creator
+ contract_resp = supabase.table("contracts") \
+ .select("id, creator_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ if contract_resp.data["creator_id"] != creator["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="Only the creator can submit deliverables"
+ )
+
+ # Verify deliverable exists and belongs to this contract
+ deliv_resp = supabase.table("contract_deliverables") \
+ .select("*") \
+ .eq("id", deliverable_id) \
+ .eq("contract_id", contract_id) \
+ .single() \
+ .execute()
+
+ if not deliv_resp.data:
+ raise HTTPException(status_code=404, detail="Deliverable not found")
+
+ deliverable = deliv_resp.data
+
+ # Check if deliverables list is approved by both parties
+ if not deliverable.get("brand_approval") or not deliverable.get("creator_approval"):
+ raise HTTPException(
+ status_code=400,
+ detail="Deliverables list must be approved by both parties before submission"
+ )
+
+ # Check if deliverable is already completed
+ if deliverable.get("status") == "completed":
+ raise HTTPException(
+ status_code=400,
+ detail="This deliverable is already completed and cannot be modified"
+ )
+
+ # Update deliverable with submission
+ update_data = {
+ "submission_url": payload.submission_url,
+ "status": "under_review",
+ "rejection_reason": None, # Clear previous rejection
+ }
+
+ update_resp = supabase.table("contract_deliverables") \
+ .update(update_data) \
+ .eq("id", deliverable_id) \
+ .execute()
+
+ if not update_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to submit deliverable"
+ )
+
+ updated = update_resp.data[0]
+
+ due_date = updated.get("due_date")
+ return DeliverableResponse(
+ id=updated.get("id"),
+ contract_id=updated.get("contract_id"),
+ description=updated.get("description"),
+ due_date=parse_datetime(due_date) if due_date else None,
+ status=updated.get("status", "under_review"),
+ submission_url=updated.get("submission_url"),
+ review_comment=updated.get("review_comment"),
+ rejection_reason=updated.get("rejection_reason"),
+ brand_approval=updated.get("brand_approval", False),
+ creator_approval=updated.get("creator_approval", False),
+ created_at=parse_datetime(updated.get("created_at")),
+ updated_at=parse_datetime(updated.get("updated_at")),
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error submitting deliverable: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/deliverables/{deliverable_id}/review",
+ response_model=DeliverableResponse
+)
+async def review_deliverable(
+ contract_id: str,
+ deliverable_id: str,
+ payload: DeliverableReview,
+ brand: dict = Depends(get_current_brand)
+):
+ """
+ Brand reviews a submitted deliverable: approve or reject.
+ If rejected, must provide a reason.
+ """
+ supabase = supabase_anon
+
+ try:
+ # Verify contract exists and belongs to this brand
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ if contract_resp.data["brand_id"] != brand["id"]:
+ raise HTTPException(
+ status_code=403,
+ detail="Only the brand can review deliverables"
+ )
+
+ # Verify deliverable exists and belongs to this contract
+ deliv_resp = supabase.table("contract_deliverables") \
+ .select("*") \
+ .eq("id", deliverable_id) \
+ .eq("contract_id", contract_id) \
+ .single() \
+ .execute()
+
+ if not deliv_resp.data:
+ raise HTTPException(status_code=404, detail="Deliverable not found")
+
+ deliverable = deliv_resp.data
+
+ # Check if deliverable is under review
+ if deliverable.get("status") != "under_review":
+ raise HTTPException(
+ status_code=400,
+ detail="Deliverable must be under review to be reviewed"
+ )
+
+ # Validate rejection reason if rejecting
+ if not payload.approved:
+ if not payload.rejection_reason or not payload.rejection_reason.strip():
+ raise HTTPException(
+ status_code=400,
+ detail="Rejection reason is required when rejecting a deliverable"
+ )
+
+ # Update deliverable
+ update_data = {
+ "status": "completed" if payload.approved else "rejected",
+ "review_comment": payload.review_comment,
+ "rejection_reason": payload.rejection_reason if not payload.approved else None,
+ }
+
+ update_resp = supabase.table("contract_deliverables") \
+ .update(update_data) \
+ .eq("id", deliverable_id) \
+ .execute()
+
+ if not update_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to review deliverable"
+ )
+
+ updated = update_resp.data[0]
+
+ return DeliverableResponse(
+ id=updated.get("id"),
+ contract_id=updated.get("contract_id"),
+ description=updated.get("description"),
+ due_date=parse_datetime(updated.get("due_date")),
+ status=updated.get("status"),
+ submission_url=updated.get("submission_url"),
+ review_comment=updated.get("review_comment"),
+ rejection_reason=updated.get("rejection_reason"),
+ brand_approval=updated.get("brand_approval", False),
+ creator_approval=updated.get("creator_approval", False),
+ created_at=parse_datetime(updated.get("created_at")),
+ updated_at=parse_datetime(updated.get("updated_at")),
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error reviewing deliverable: {str(e)}"
+ ) from e
+
+
+# ============================================================================
+# CONTRACT VERSIONING ENDPOINTS
+# ============================================================================
+
+class ContractVersionCreate(BaseModel):
+ """Schema for creating a contract version."""
+ file_url: str = Field(..., min_length=1, max_length=2000)
+ change_reason: Optional[str] = Field(None, max_length=2000)
+
+
+class ContractVersionResponse(BaseModel):
+ """Schema for contract version response."""
+ id: str
+ contract_id: str
+ version_number: int
+ file_url: str
+ uploaded_by: Optional[str]
+ uploaded_at: datetime
+ status: str
+ brand_approval: bool
+ creator_approval: bool
+ change_reason: Optional[str]
+ is_current: bool
+
+
+class VersionApprovalRequest(BaseModel):
+ """Schema for approving/rejecting a version."""
+ approved: bool
+
+
+@router.get(
+ "/contracts/{contract_id}/versions",
+ response_model=List[ContractVersionResponse]
+)
+async def get_contract_versions(
+ contract_id: str,
+ user: dict = Depends(get_current_user)
+):
+ """Fetch all versions for a contract."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can view contract versions"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+
+ # Verify user has access to this contract
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ # Fetch versions
+ versions_resp = supabase.table("contract_versions") \
+ .select("*") \
+ .eq("contract_id", contract_id) \
+ .order("version_number", desc=False) \
+ .execute()
+
+ versions = []
+ for version in (versions_resp.data or []):
+ versions.append({
+ "id": version.get("id"),
+ "contract_id": version.get("contract_id"),
+ "version_number": version.get("version_number", 0),
+ "file_url": version.get("file_url"),
+ "uploaded_by": version.get("uploaded_by"),
+ "uploaded_at": parse_datetime(version.get("uploaded_at")),
+ "status": version.get("status", "pending"),
+ "brand_approval": version.get("brand_approval", False),
+ "creator_approval": version.get("creator_approval", False),
+ "change_reason": version.get("change_reason"),
+ "is_current": version.get("is_current", False),
+ })
+
+ return [ContractVersionResponse.model_validate(v) for v in versions]
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching contract versions: {str(e)}"
+ ) from e
+
+
+@router.get(
+ "/contracts/{contract_id}/versions/current",
+ response_model=ContractVersionResponse
+)
+async def get_current_contract_version(
+ contract_id: str,
+ user: dict = Depends(get_current_user)
+):
+ """Fetch the current (finalized) contract version."""
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can view contract versions"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id, current_version_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+
+ # Verify user has access
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ # Get current version
+ if contract.get("current_version_id"):
+ version_resp = supabase.table("contract_versions") \
+ .select("*") \
+ .eq("id", contract["current_version_id"]) \
+ .single() \
+ .execute()
+
+ if version_resp.data:
+ version = version_resp.data
+ return ContractVersionResponse(
+ id=version.get("id"),
+ contract_id=version.get("contract_id"),
+ version_number=version.get("version_number", 0),
+ file_url=version.get("file_url"),
+ uploaded_by=version.get("uploaded_by"),
+ uploaded_at=parse_datetime(version.get("uploaded_at")),
+ status=version.get("status", "final"),
+ brand_approval=version.get("brand_approval", False),
+ creator_approval=version.get("creator_approval", False),
+ change_reason=version.get("change_reason"),
+ is_current=version.get("is_current", False),
+ )
+
+ raise HTTPException(
+ status_code=404,
+ detail="No current contract version found. Contract may not be finalized yet."
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching current contract version: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/versions",
+ response_model=ContractVersionResponse,
+ status_code=201
+)
+async def create_contract_version(
+ contract_id: str,
+ payload: ContractVersionCreate,
+ user: dict = Depends(get_current_user)
+):
+ """
+ Create a new contract version (amendment).
+ Either brand or creator can initiate an amendment.
+ """
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can create contract versions"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id, current_version_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+
+ # Verify user has access
+ profile_id = None
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ profile_id = brand_profile["id"]
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ profile_id = creator_profile["id"]
+
+ # Get the highest version number
+ versions_resp = supabase.table("contract_versions") \
+ .select("version_number") \
+ .eq("contract_id", contract_id) \
+ .order("version_number", desc=True) \
+ .limit(1) \
+ .execute()
+
+ next_version = 1
+ if versions_resp.data and len(versions_resp.data) > 0:
+ next_version = versions_resp.data[0].get("version_number", 0) + 1
+
+ # Create new version
+ version_data = {
+ "contract_id": contract_id,
+ "version_number": next_version,
+ "file_url": payload.file_url,
+ "uploaded_by": profile_id,
+ "status": "pending",
+ "brand_approval": False,
+ "creator_approval": False,
+ "change_reason": payload.change_reason,
+ "is_current": False,
+ }
+
+ insert_resp = supabase.table("contract_versions") \
+ .insert(version_data) \
+ .execute()
+
+ if not insert_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to create contract version"
+ )
+
+ version_record = insert_resp.data[0]
+
+ return ContractVersionResponse(
+ id=version_record.get("id"),
+ contract_id=version_record.get("contract_id"),
+ version_number=version_record.get("version_number", next_version),
+ file_url=version_record.get("file_url"),
+ uploaded_by=version_record.get("uploaded_by"),
+ uploaded_at=parse_datetime(version_record.get("uploaded_at")),
+ status=version_record.get("status", "pending"),
+ brand_approval=version_record.get("brand_approval", False),
+ creator_approval=version_record.get("creator_approval", False),
+ change_reason=version_record.get("change_reason"),
+ is_current=version_record.get("is_current", False),
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error creating contract version: {str(e)}"
+ ) from e
+
+
+@router.post(
+ "/contracts/{contract_id}/versions/{version_id}/approve",
+ response_model=ContractVersionResponse
+)
+async def approve_contract_version(
+ contract_id: str,
+ version_id: str,
+ payload: VersionApprovalRequest,
+ user: dict = Depends(get_current_user)
+):
+ """
+ Brand or Creator approves/rejects a contract version.
+ When both parties approve, the version becomes final and current.
+ """
+ supabase = supabase_anon
+ role = user.get("role")
+
+ if role not in ("Brand", "Creator"):
+ raise HTTPException(
+ status_code=403,
+ detail="Only brands and creators can approve contract versions"
+ )
+
+ try:
+ # Verify contract exists and user has access
+ contract_resp = supabase.table("contracts") \
+ .select("id, brand_id, creator_id") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+
+ # Verify user has access
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ approval_field = "brand_approval"
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ approval_field = "creator_approval"
+
+ # Verify version exists
+ version_resp = supabase.table("contract_versions") \
+ .select("*") \
+ .eq("id", version_id) \
+ .eq("contract_id", contract_id) \
+ .single() \
+ .execute()
+
+ if not version_resp.data:
+ raise HTTPException(status_code=404, detail="Contract version not found")
+
+ version = version_resp.data
+
+ # Update approval
+ update_data = {approval_field: payload.approved}
+ if not payload.approved:
+ # If rejecting, set status to rejected
+ update_data["status"] = "rejected"
+ else:
+ # If approving, check if both parties have approved
+ brand_approved = version.get("brand_approval", False) if role == "Creator" else payload.approved
+ creator_approved = version.get("creator_approval", False) if role == "Brand" else payload.approved
+
+ if brand_approved and creator_approved:
+ # Both parties approved - finalize this version
+ # First, set all other versions' is_current to false
+ supabase.table("contract_versions") \
+ .update({"is_current": False}) \
+ .eq("contract_id", contract_id) \
+ .neq("id", version_id) \
+ .execute()
+
+ # Mark this version as current and final
+ update_data["status"] = "final"
+ update_data["is_current"] = True
+
+ # Update contract's current_version_id
+ supabase.table("contracts") \
+ .update({"current_version_id": version_id}) \
+ .eq("id", contract_id) \
+ .execute()
+
+ update_resp = supabase.table("contract_versions") \
+ .update(update_data) \
+ .eq("id", version_id) \
+ .execute()
+
+ if not update_resp.data:
+ raise HTTPException(
+ status_code=500,
+ detail="Failed to update contract version"
+ )
+
+ updated = update_resp.data[0]
+
+ return ContractVersionResponse(
+ id=updated.get("id"),
+ contract_id=updated.get("contract_id"),
+ version_number=updated.get("version_number", 0),
+ file_url=updated.get("file_url"),
+ uploaded_by=updated.get("uploaded_by"),
+ uploaded_at=parse_datetime(updated.get("uploaded_at")),
+ status=updated.get("status", "pending"),
+ brand_approval=updated.get("brand_approval", False),
+ creator_approval=updated.get("creator_approval", False),
+ change_reason=updated.get("change_reason"),
+ is_current=updated.get("is_current", False),
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error approving contract version: {str(e)}"
+ ) from e
+
+
+@router.get("/proposals/draft", response_model=dict)
+async def draft_proposal_content(
+ campaign_id: str = Query(..., description="Campaign ID"),
+ creator_id: str = Query(..., description="Creator ID"),
+ content_idea: Optional[str] = Query(None, description="Content idea"),
+ ideal_pricing: Optional[str] = Query(None, description="Ideal pricing"),
+ brand: dict = Depends(get_current_brand)
+):
+ """Use AI to draft proposal content based on brand, campaign, and creator details."""
+ supabase = supabase_anon
+ brand_id = brand['id']
+
+ try:
+ # Fetch campaign
+ campaign_resp = supabase.table("campaigns") \
+ .select("*") \
+ .eq("id", campaign_id) \
+ .eq("brand_id", brand_id) \
+ .single() \
+ .execute()
+
+ if not campaign_resp.data:
+ raise HTTPException(status_code=404, detail="Campaign not found")
+
+ campaign = campaign_resp.data
+
+ # Fetch brand details
+ brand_resp = supabase.table("brands") \
+ .select("*") \
+ .eq("id", brand_id) \
+ .single() \
+ .execute()
+
+ brand_data = brand_resp.data if brand_resp.data else {}
+
+ # Fetch creator details
+ creator_resp = supabase.table("creators") \
+ .select("*") \
+ .eq("id", creator_id) \
+ .eq("is_active", True) \
+ .single() \
+ .execute()
+
+ if not creator_resp.data:
+ raise HTTPException(status_code=404, detail="Creator not found")
+
+ creator = creator_resp.data
+
+ # Build prompt for AI
+ prompt = f"""You are a professional brand partnership strategist. Draft a compelling collaboration proposal email.
+
+BRAND INFORMATION:
+- Company: {brand_data.get('company_name', 'Unknown')}
+- Industry: {brand_data.get('industry', 'N/A')}
+- Description: {brand_data.get('company_description', 'N/A')}
+- Brand Values: {', '.join(brand_data.get('brand_values', []) or [])}
+- Brand Voice: {brand_data.get('brand_voice', 'Professional')}
+
+CAMPAIGN DETAILS:
+- Title: {campaign.get('title', 'N/A')}
+- Description: {campaign.get('description', campaign.get('short_description', 'N/A'))}
+- Platforms: {', '.join(campaign.get('platforms', []) or [])}
+- Budget Range: {campaign.get('budget_min', 0)} - {campaign.get('budget_max', 0)} INR
+- Preferred Niches: {', '.join(campaign.get('preferred_creator_niches', []) or [])}
+
+CREATOR PROFILE:
+- Name: {creator.get('display_name', 'N/A')}
+- Niche: {creator.get('primary_niche', 'N/A')}
+- Followers: {creator.get('total_followers', 0)}
+- Engagement Rate: {creator.get('engagement_rate', 0)}%
+- Bio: {creator.get('bio', 'N/A')}
+
+CONTENT IDEA: {content_idea or 'Not specified'}
+IDEAL PRICING: {ideal_pricing or 'To be discussed'}
+
+Create a professional, personalized proposal email with:
+1. A compelling subject line
+2. An engaging opening that shows you've researched the creator
+3. Clear explanation of the campaign and collaboration opportunity
+4. Specific content ideas or deliverables
+5. Proposed compensation (if ideal pricing provided, use it as a guide)
+6. Next steps and call to action
+
+Return your response as JSON with this structure:
+{{
+ "subject": "Subject line here",
+ "message": "Full proposal message here"
+}}"""
+
+ # Call Groq API
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert at writing professional brand partnership proposals. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.7,
+ max_completion_tokens=1500,
+ top_p=1,
+ stream=False,
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ try:
+ draft = json.loads(content)
+ return {
+ "subject": draft.get("subject", f"Collaboration Opportunity: {campaign.get('title', 'Campaign')}"),
+ "message": draft.get("message", "We would love to collaborate with you on this campaign.")
+ }
+ except json.JSONDecodeError:
+ # Fallback if JSON parsing fails
+ return {
+ "subject": f"Collaboration Opportunity: {campaign.get('title', 'Campaign')}",
+ "message": content if content else "We would love to collaborate with you on this campaign."
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error drafting proposal: {str(e)}"
+ ) from e
+
+
+# ============================================================================
+# NEGOTIATION AI FEATURES
+# ============================================================================
+
+class SentimentAnalysisRequest(BaseModel):
+ """Request for sentiment analysis of negotiation messages."""
+ messages: List[str] = Field(..., description="List of messages to analyze")
+
+
+class SentimentAnalysisResponse(BaseModel):
+ """Response for sentiment analysis."""
+ overall_sentiment: str = Field(..., description="Overall sentiment: positive, neutral, negative, or mixed")
+ sentiment_score: float = Field(..., description="Sentiment score from -1 (negative) to 1 (positive)")
+ detected_tone: List[str] = Field(default_factory=list, description="Detected tones: e.g., 'hesitant', 'confident', 'conflict'")
+ guidance: str = Field(..., description="Actionable guidance based on sentiment")
+ alerts: List[str] = Field(default_factory=list, description="Alerts for concerning patterns")
+
+
+@router.post("/proposals/{proposal_id}/negotiation/analyze-sentiment", response_model=SentimentAnalysisResponse)
+async def analyze_negotiation_sentiment(
+ proposal_id: str,
+ user: dict = Depends(get_current_user)
+):
+ """Analyze sentiment of negotiation messages to detect tone and provide guidance."""
+ supabase = supabase_anon
+ proposal = fetch_proposal_by_id(proposal_id)
+
+ # Verify user has access
+ user_role = user.get("role")
+ if user_role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != proposal["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ elif user_role == "Creator":
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != proposal["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ thread = normalize_negotiation_thread(proposal.get("negotiation_thread"))
+ messages = [entry.get("message", "") for entry in thread if entry.get("type") == "message" and entry.get("message")]
+
+ if not messages:
+ return SentimentAnalysisResponse(
+ overall_sentiment="neutral",
+ sentiment_score=0.0,
+ detected_tone=[],
+ guidance="No messages found in this negotiation yet.",
+ alerts=[]
+ )
+
+ try:
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ messages_text = "\n".join([f"Message {i+1}: {msg}" for i, msg in enumerate(messages)])
+
+ prompt = f"""Analyze the sentiment and tone of these negotiation messages from a business collaboration context:
+
+{messages_text}
+
+Provide a comprehensive sentiment analysis including:
+1. Overall sentiment (positive, neutral, negative, or mixed)
+2. Sentiment score from -1 (very negative) to 1 (very positive)
+3. Detected tones (e.g., hesitant, confident, conflict, enthusiastic, defensive, collaborative)
+4. Actionable guidance for the user on how to proceed
+5. Any alerts for concerning patterns (conflict, hesitation, negative signals)
+
+Return your response as JSON with this exact structure:
+{{
+ "overall_sentiment": "positive|neutral|negative|mixed",
+ "sentiment_score": 0.75,
+ "detected_tone": ["confident", "collaborative"],
+ "guidance": "The negotiation shows positive momentum. Consider...",
+ "alerts": []
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert business communication analyst. Analyze negotiation messages and provide actionable insights. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.3,
+ max_completion_tokens=800,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ return SentimentAnalysisResponse(
+ overall_sentiment=result.get("overall_sentiment", "neutral"),
+ sentiment_score=float(result.get("sentiment_score", 0.0)),
+ detected_tone=result.get("detected_tone", []),
+ guidance=result.get("guidance", "Continue the negotiation with professional communication."),
+ alerts=result.get("alerts", [])
+ )
+
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Failed to parse AI response")
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error analyzing sentiment: {str(e)}"
+ ) from e
+
+
+class MessageDraftRequest(BaseModel):
+ """Request for AI message drafting assistance."""
+ context: str = Field(..., description="Context or intent for the message")
+ tone: Optional[str] = Field("professional", description="Desired tone: professional, polite, persuasive, friendly")
+ current_negotiation_state: Optional[str] = Field(None, description="Current state of negotiation")
+
+
+class MessageDraftResponse(BaseModel):
+ """Response for message drafting."""
+ draft: str = Field(..., description="AI-generated message draft")
+ suggestions: List[str] = Field(default_factory=list, description="Additional suggestions or tips")
+
+
+@router.post("/proposals/{proposal_id}/negotiation/draft-message", response_model=MessageDraftResponse)
+async def draft_negotiation_message(
+ proposal_id: str,
+ payload: MessageDraftRequest,
+ user: dict = Depends(get_current_user)
+):
+ """AI assistance for drafting negotiation messages."""
+ supabase = supabase_anon
+ proposal = fetch_proposal_by_id(proposal_id)
+
+ # Verify user has access
+ user_role = user.get("role")
+ sender_name = "User"
+ if user_role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != proposal["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ sender_name = brand_profile.get("company_name", "Brand")
+ elif user_role == "Creator":
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != proposal["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ sender_name = creator_profile.get("display_name", "Creator")
+ else:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ thread = normalize_negotiation_thread(proposal.get("negotiation_thread"))
+ recent_messages = thread[-5:] if len(thread) > 5 else thread
+ conversation_context = "\n".join([
+ f"{entry.get('sender_role')}: {entry.get('message', '')}"
+ for entry in recent_messages
+ if entry.get("type") == "message"
+ ])
+
+ try:
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ prompt = f"""You are helping {sender_name} draft a negotiation message.
+
+PROPOSAL CONTEXT:
+- Subject: {proposal.get('subject', 'N/A')}
+- Campaign: {proposal.get('campaign_title', 'N/A')}
+
+RECENT CONVERSATION:
+{conversation_context if conversation_context else 'This is the start of the negotiation.'}
+
+USER'S INTENT:
+{payload.context}
+
+DESIRED TONE: {payload.tone}
+
+CURRENT NEGOTIATION STATE: {payload.current_negotiation_state or 'Active negotiation'}
+
+Draft a {payload.tone} negotiation message that:
+1. Is clear and professional
+2. Addresses the user's intent
+3. Maintains a {payload.tone} tone
+4. Is appropriate for the negotiation context
+5. Moves the conversation forward constructively
+
+Return your response as JSON with this structure:
+{{
+ "draft": "The complete message draft here",
+ "suggestions": ["Tip 1", "Tip 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert at writing professional business negotiation messages. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.7,
+ max_completion_tokens=600,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ return MessageDraftResponse(
+ draft=result.get("draft", "I would like to discuss the proposal further."),
+ suggestions=result.get("suggestions", [])
+ )
+
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Failed to parse AI response")
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error drafting message: {str(e)}"
+ ) from e
+
+
+class DealProbabilityResponse(BaseModel):
+ """Response for deal probability prediction."""
+ probability: float = Field(..., description="Probability of successful deal (0.0 to 1.0)")
+ confidence: str = Field(..., description="Confidence level: high, medium, low")
+ factors: List[str] = Field(default_factory=list, description="Key factors influencing the prediction")
+ recommendations: List[str] = Field(default_factory=list, description="Recommendations to improve deal probability")
+
+
+@router.get("/proposals/{proposal_id}/negotiation/deal-probability", response_model=DealProbabilityResponse)
+async def predict_deal_probability(
+ proposal_id: str,
+ user: dict = Depends(get_current_user)
+):
+ """Predict the likelihood of a negotiation resulting in a successful deal."""
+ supabase = supabase_anon
+ proposal = fetch_proposal_by_id(proposal_id)
+
+ # Verify user has access
+ user_role = user.get("role")
+ if user_role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != proposal["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ elif user_role == "Creator":
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != proposal["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ thread = normalize_negotiation_thread(proposal.get("negotiation_thread"))
+ messages = [entry.get("message", "") for entry in thread if entry.get("type") == "message"]
+
+ # Get historical data (simplified - could be enhanced with actual historical success rates)
+ try:
+ # Count similar successful negotiations (simplified approach)
+ similar_proposals = supabase.table("proposals") \
+ .select("id, status, negotiation_status") \
+ .eq("brand_id", proposal["brand_id"]) \
+ .eq("creator_id", proposal["creator_id"]) \
+ .in_("negotiation_status", ["finalized", "open"]) \
+ .execute()
+
+ historical_success_rate = 0.5 # Default
+ if similar_proposals.data:
+ finalized = sum(1 for p in similar_proposals.data if p.get("negotiation_status") == "finalized")
+ historical_success_rate = finalized / len(similar_proposals.data) if similar_proposals.data else 0.5
+ except:
+ historical_success_rate = 0.5
+
+ try:
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ conversation_summary = "\n".join([f"Message {i+1}: {msg}" for i, msg in enumerate(messages)]) if messages else "No messages yet."
+
+ prompt = f"""Analyze this business negotiation and predict the probability of a successful deal.
+
+PROPOSAL DETAILS:
+- Subject: {proposal.get('subject', 'N/A')}
+- Status: {proposal.get('status', 'N/A')}
+- Negotiation Status: {proposal.get('negotiation_status', 'N/A')}
+- Proposed Amount: {proposal.get('proposed_amount', 'N/A')}
+- Version: {proposal.get('version', 1)}
+
+CONVERSATION HISTORY:
+{conversation_summary}
+
+HISTORICAL SUCCESS RATE: {historical_success_rate:.2%}
+
+CURRENT TERMS:
+{json.dumps(proposal.get('current_terms', {}), indent=2) if proposal.get('current_terms') else 'No terms set yet.'}
+
+Based on:
+1. Conversation tone and engagement
+2. Progress in negotiation
+3. Terms alignment
+4. Historical patterns
+5. Communication quality
+
+Predict the probability (0.0 to 1.0) of this negotiation resulting in a successful deal.
+
+Return your response as JSON with this structure:
+{{
+ "probability": 0.75,
+ "confidence": "high|medium|low",
+ "factors": ["Factor 1", "Factor 2"],
+ "recommendations": ["Recommendation 1", "Recommendation 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert business analyst specializing in deal prediction. Analyze negotiations and provide probability estimates. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.2,
+ max_completion_tokens=600,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ probability = float(result.get("probability", 0.5))
+ # Clamp probability between 0 and 1
+ probability = max(0.0, min(1.0, probability))
+
+ return DealProbabilityResponse(
+ probability=probability,
+ confidence=result.get("confidence", "medium"),
+ factors=result.get("factors", []),
+ recommendations=result.get("recommendations", [])
+ )
+
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Failed to parse AI response")
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error predicting deal probability: {str(e)}"
+ ) from e
+
+
+class TranslationRequest(BaseModel):
+ """Request for message translation."""
+ text: str = Field(..., description="Text to translate")
+ target_language: str = Field(..., description="Target language code (e.g., 'es', 'fr', 'de', 'zh')")
+ source_language: Optional[str] = Field(None, description="Source language code (auto-detect if not provided)")
+
+
+class TranslationResponse(BaseModel):
+ """Response for translation."""
+ translated_text: str = Field(..., description="Translated text")
+ detected_language: Optional[str] = Field(None, description="Detected source language")
+ confidence: Optional[float] = Field(None, description="Translation confidence score")
+
+
+@router.post("/proposals/{proposal_id}/negotiation/translate", response_model=TranslationResponse)
+async def translate_negotiation_message(
+ proposal_id: str,
+ payload: TranslationRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Translate negotiation messages for cross-border negotiations."""
+ supabase = supabase_anon
+ proposal = fetch_proposal_by_id(proposal_id)
+
+ # Verify user has access
+ user_role = user.get("role")
+ if user_role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != proposal["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ elif user_role == "Creator":
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != proposal["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ if not payload.text.strip():
+ raise HTTPException(status_code=400, detail="Text to translate cannot be empty")
+
+ # Language code mapping
+ language_names = {
+ "es": "Spanish", "fr": "French", "de": "German", "zh": "Chinese",
+ "ja": "Japanese", "ko": "Korean", "pt": "Portuguese", "it": "Italian",
+ "ru": "Russian", "ar": "Arabic", "hi": "Hindi", "nl": "Dutch",
+ "sv": "Swedish", "pl": "Polish", "tr": "Turkish"
+ }
+
+ target_language_name = language_names.get(payload.target_language.lower(), payload.target_language)
+ source_language_name = language_names.get(payload.source_language.lower(), payload.source_language) if payload.source_language else None
+
+ try:
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ prompt = f"""Translate the following business negotiation message to {target_language_name}.
+
+Maintain:
+- Professional tone
+- Business context and meaning
+- All numbers, dates, and technical terms accurately
+- Cultural appropriateness for business communication
+
+Source text:
+{payload.text}
+
+Provide the translation and detect the source language if not specified.
+
+Return your response as JSON with this structure:
+{{
+ "translated_text": "Translated text here",
+ "detected_language": "en",
+ "confidence": 0.95
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert translator specializing in business and professional communication. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.3,
+ max_completion_tokens=500,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ return TranslationResponse(
+ translated_text=result.get("translated_text", payload.text),
+ detected_language=result.get("detected_language") or payload.source_language,
+ confidence=float(result.get("confidence", 0.9)) if result.get("confidence") else None
+ )
+
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Failed to parse AI response")
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error translating message: {str(e)}"
+ ) from e
+
+
+# ============================================================================
+# CONTRACT AI FEATURES
+# ============================================================================
+
+class ContractQuestionRequest(BaseModel):
+ """Request for contract question answering."""
+ question: str = Field(..., description="Question about the contract")
+
+
+class ContractQuestionResponse(BaseModel):
+ """Response for contract question."""
+ answer: str = Field(..., description="AI-generated answer to the question")
+ relevant_clauses: List[str] = Field(default_factory=list, description="Relevant contract clauses referenced")
+
+
+@router.post("/contracts/{contract_id}/ask-question", response_model=ContractQuestionResponse)
+async def ask_contract_question(
+ contract_id: str,
+ payload: ContractQuestionRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Allow users to ask questions about the contract and get AI-powered answers."""
+ supabase = supabase_anon
+
+ # Verify access
+ contract_resp = supabase.table("contracts") \
+ .select("*, proposals(*)") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+ role = user.get("role")
+
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ elif role == "Creator":
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ try:
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ contract_terms = json.dumps(contract.get("terms", {}), indent=2)
+ proposal = contract.get("proposals", {}) if isinstance(contract.get("proposals"), dict) else {}
+
+ prompt = f"""You are a contract analysis assistant. Answer the user's question about this contract.
+
+CONTRACT TERMS:
+{contract_terms}
+
+PROPOSAL CONTEXT:
+- Subject: {proposal.get('subject', 'N/A')}
+- Campaign: {proposal.get('campaign_title', 'N/A')}
+- Proposed Amount: {proposal.get('proposed_amount', 'N/A')}
+
+USER'S QUESTION:
+{payload.question}
+
+Provide a clear, accurate answer based on the contract terms. If the information is not in the contract, say so. Also identify which specific clauses or sections are relevant to the answer.
+
+Return your response as JSON with this structure:
+{{
+ "answer": "Clear answer to the question",
+ "relevant_clauses": ["Clause 1", "Clause 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert contract analyst. Answer questions accurately based on contract terms. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.3,
+ max_completion_tokens=800,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ return ContractQuestionResponse(
+ answer=result.get("answer", "I couldn't find a clear answer to that question in the contract."),
+ relevant_clauses=result.get("relevant_clauses", [])
+ )
+
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Failed to parse AI response")
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error answering question: {str(e)}"
+ ) from e
+
+
+class ContractTemplateRequest(BaseModel):
+ """Request for contract template generation."""
+ deal_type: str = Field(..., description="Type of deal (e.g., 'sponsored content', 'brand ambassadorship')")
+ deliverables: Optional[List[str]] = Field(default_factory=list, description="List of deliverables")
+ payment_amount: Optional[float] = Field(None, description="Payment amount")
+ duration: Optional[str] = Field(None, description="Contract duration")
+ additional_requirements: Optional[str] = Field(None, description="Additional requirements or notes")
+
+
+class ContractTemplateResponse(BaseModel):
+ """Response for contract template."""
+ template: Dict[str, Any] = Field(..., description="Generated contract template as JSON")
+ suggestions: List[str] = Field(default_factory=list, description="Suggestions for the contract")
+
+
+@router.post("/contracts/generate-template", response_model=ContractTemplateResponse)
+async def generate_contract_template(
+ payload: ContractTemplateRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Generate draft contract templates for new deals based on best practices."""
+ if user.get("role") not in ("Brand", "Creator"):
+ raise HTTPException(status_code=403, detail="Only brands and creators can generate templates")
+
+ try:
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ # Get user's previous contracts for reference
+ supabase = supabase_anon
+ role = user.get("role")
+ previous_contracts = []
+
+ try:
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if brand_profile:
+ contracts_resp = supabase.table("contracts") \
+ .select("terms") \
+ .eq("brand_id", brand_profile["id"]) \
+ .limit(5) \
+ .execute()
+ previous_contracts = [c.get("terms") for c in (contracts_resp.data or []) if c.get("terms")]
+ else:
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if creator_profile:
+ contracts_resp = supabase.table("contracts") \
+ .select("terms") \
+ .eq("creator_id", creator_profile["id"]) \
+ .limit(5) \
+ .execute()
+ previous_contracts = [c.get("terms") for c in (contracts_resp.data or []) if c.get("terms")]
+ except:
+ pass # Continue without previous contracts if fetch fails
+
+ previous_examples = json.dumps(previous_contracts[:3], indent=2) if previous_contracts else "None available"
+
+ prompt = f"""Generate a professional contract template for a brand-creator collaboration deal.
+
+DEAL TYPE: {payload.deal_type}
+DELIVERABLES: {', '.join(payload.deliverables) if payload.deliverables else 'To be specified'}
+PAYMENT AMOUNT: {payload.payment_amount or 'To be negotiated'}
+DURATION: {payload.duration or 'To be specified'}
+ADDITIONAL REQUIREMENTS: {payload.additional_requirements or 'None'}
+
+PREVIOUS CONTRACT EXAMPLES (for reference):
+{previous_examples}
+
+Generate a comprehensive contract template that includes:
+1. Parties involved
+2. Scope of work and deliverables
+3. Payment terms and schedule
+4. Timeline and deadlines
+5. Content usage rights
+6. Exclusivity clauses (if applicable)
+7. Termination conditions
+8. Dispute resolution
+9. Confidentiality
+10. Any other relevant standard clauses
+
+Return your response as JSON with this structure:
+{{
+ "template": {{
+ "parties": {{"brand": "...", "creator": "..."}},
+ "scope_of_work": "...",
+ "deliverables": [...],
+ "payment_terms": {{"amount": ..., "schedule": "..."}},
+ "timeline": "...",
+ "content_rights": "...",
+ "exclusivity": "...",
+ "termination": "...",
+ "dispute_resolution": "...",
+ "confidentiality": "..."
+ }},
+ "suggestions": ["Suggestion 1", "Suggestion 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert contract lawyer specializing in influencer marketing agreements. Generate professional contract templates. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.5,
+ max_completion_tokens=2000,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ return ContractTemplateResponse(
+ template=result.get("template", {}),
+ suggestions=result.get("suggestions", [])
+ )
+
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Failed to parse AI response")
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error generating template: {str(e)}"
+ ) from e
+
+
+class ContractTranslationRequest(BaseModel):
+ """Request for contract translation."""
+ target_language: str = Field(..., description="Target language code (e.g., 'es', 'fr', 'de')")
+
+
+class ContractTranslationResponse(BaseModel):
+ """Response for contract translation."""
+ translated_terms: Dict[str, Any] = Field(..., description="Translated contract terms")
+ detected_language: Optional[str] = Field(None, description="Detected source language")
+
+
+@router.post("/contracts/{contract_id}/translate", response_model=ContractTranslationResponse)
+async def translate_contract(
+ contract_id: str,
+ payload: ContractTranslationRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Translate contracts into the user's preferred language."""
+ supabase = supabase_anon
+
+ # Verify access
+ contract_resp = supabase.table("contracts") \
+ .select("*") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+ role = user.get("role")
+
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ elif role == "Creator":
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ # Language code mapping
+ language_names = {
+ "es": "Spanish", "fr": "French", "de": "German", "zh": "Chinese",
+ "ja": "Japanese", "ko": "Korean", "pt": "Portuguese", "it": "Italian",
+ "ru": "Russian", "ar": "Arabic", "hi": "Hindi", "nl": "Dutch",
+ "sv": "Swedish", "pl": "Polish", "tr": "Turkish"
+ }
+
+ target_language_name = language_names.get(payload.target_language.lower(), payload.target_language)
+
+ try:
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ contract_terms = json.dumps(contract.get("terms", {}), indent=2)
+
+ prompt = f"""Translate the following contract terms to {target_language_name}.
+
+Maintain:
+- Legal accuracy and precision
+- Professional business tone
+- All numbers, dates, and technical terms exactly as they are
+- Contract structure and formatting
+- Cultural appropriateness for business communication
+
+CONTRACT TERMS (JSON):
+{contract_terms}
+
+Return the translated contract as JSON with the same structure, and detect the source language.
+
+Return your response as JSON with this structure:
+{{
+ "translated_terms": {{...translated contract JSON...}},
+ "detected_language": "en"
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert legal translator specializing in business contracts. Translate contracts accurately while maintaining legal precision. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.2,
+ max_completion_tokens=3000,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ return ContractTranslationResponse(
+ translated_terms=result.get("translated_terms", contract.get("terms", {})),
+ detected_language=result.get("detected_language", "en")
+ )
+
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Failed to parse AI response")
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error translating contract: {str(e)}"
+ ) from e
+
+
+class ClauseExplanationRequest(BaseModel):
+ """Request for clause explanation."""
+ clause_text: str = Field(..., description="The clause text to explain")
+ clause_context: Optional[str] = Field(None, description="Context about where this clause appears in the contract")
+
+
+class ClauseExplanationResponse(BaseModel):
+ """Response for clause explanation."""
+ explanation: str = Field(..., description="Plain-language explanation of the clause")
+ key_points: List[str] = Field(default_factory=list, description="Key points to understand")
+ implications: List[str] = Field(default_factory=list, description="What this means for the user")
+
+
+@router.post("/contracts/{contract_id}/explain-clause", response_model=ClauseExplanationResponse)
+async def explain_contract_clause(
+ contract_id: str,
+ payload: ClauseExplanationRequest,
+ user: dict = Depends(get_current_user)
+):
+ """Provide plain-language explanations for complex legal clauses."""
+ supabase = supabase_anon
+
+ # Verify access
+ contract_resp = supabase.table("contracts") \
+ .select("*") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+ role = user.get("role")
+
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ elif role == "Creator":
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ try:
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ contract_terms = json.dumps(contract.get("terms", {}), indent=2)
+ user_role_label = "creator" if role == "Creator" else "brand"
+
+ prompt = f"""Explain this contract clause in plain, easy-to-understand language for a {user_role_label}.
+
+CONTRACT TERMS (for context):
+{contract_terms}
+
+CLAUSE TO EXPLAIN:
+{payload.clause_text}
+
+CONTEXT: {payload.clause_context or 'General contract clause'}
+
+Provide:
+1. A clear, plain-language explanation of what this clause means
+2. Key points the user should understand
+3. What this means for their rights and responsibilities
+
+Use simple language, avoid legal jargon, and be specific about what the user needs to know.
+
+Return your response as JSON with this structure:
+{{
+ "explanation": "Clear explanation in plain language",
+ "key_points": ["Point 1", "Point 2"],
+ "implications": ["Implication 1", "Implication 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are a legal educator who explains complex contract clauses in simple, understandable terms. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.3,
+ max_completion_tokens=800,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ return ClauseExplanationResponse(
+ explanation=result.get("explanation", "Unable to explain this clause."),
+ key_points=result.get("key_points", []),
+ implications=result.get("implications", [])
+ )
+
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Failed to parse AI response")
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error explaining clause: {str(e)}"
+ ) from e
+
+
+class ContractSummaryResponse(BaseModel):
+ """Response for contract summarization."""
+ summary: str = Field(..., description="Concise summary of the contract")
+ key_terms: Dict[str, Any] = Field(..., description="Key terms extracted (payment, timeline, deliverables, etc.)")
+ obligations: Dict[str, List[str]] = Field(..., description="Obligations for each party")
+ important_dates: List[str] = Field(default_factory=list, description="Important dates and deadlines")
+
+
+@router.get("/contracts/{contract_id}/summarize", response_model=ContractSummaryResponse)
+async def summarize_contract(
+ contract_id: str,
+ user: dict = Depends(get_current_user)
+):
+ """AI can generate concise summaries of lengthy contracts, highlighting key terms, payment details, and obligations."""
+ supabase = supabase_anon
+
+ # Verify access
+ contract_resp = supabase.table("contracts") \
+ .select("*, proposals(*)") \
+ .eq("id", contract_id) \
+ .single() \
+ .execute()
+
+ if not contract_resp.data:
+ raise HTTPException(status_code=404, detail="Contract not found")
+
+ contract = contract_resp.data
+ role = user.get("role")
+
+ if role == "Brand":
+ brand_profile = fetch_brand_profile_by_user_id(user["id"])
+ if not brand_profile or brand_profile.get("id") != contract["brand_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ elif role == "Creator":
+ creator_profile = fetch_creator_profile_by_user_id(user["id"])
+ if not creator_profile or creator_profile.get("id") != contract["creator_id"]:
+ raise HTTPException(status_code=403, detail="Access denied")
+ else:
+ raise HTTPException(status_code=403, detail="Access denied")
+
+ try:
+ if not settings.groq_api_key:
+ raise HTTPException(status_code=500, detail="GROQ API key not configured")
+
+ groq_client = Groq(api_key=settings.groq_api_key)
+
+ contract_terms = json.dumps(contract.get("terms", {}), indent=2)
+ proposal = contract.get("proposals", {}) if isinstance(contract.get("proposals"), dict) else {}
+
+ prompt = f"""Create a concise, easy-to-understand summary of this contract.
+
+CONTRACT TERMS:
+{contract_terms}
+
+PROPOSAL CONTEXT:
+- Subject: {proposal.get('subject', 'N/A')}
+- Campaign: {proposal.get('campaign_title', 'N/A')}
+
+Generate a summary that highlights:
+1. Overall purpose and scope of the agreement
+2. Key terms (payment amount, schedule, deliverables, timeline)
+3. Obligations for each party (brand and creator)
+4. Important dates and deadlines
+5. Key rights and responsibilities
+
+Return your response as JSON with this structure:
+{{
+ "summary": "Overall summary paragraph",
+ "key_terms": {{
+ "payment": "...",
+ "timeline": "...",
+ "deliverables": [...],
+ "content_rights": "..."
+ }},
+ "obligations": {{
+ "brand": ["Obligation 1", "Obligation 2"],
+ "creator": ["Obligation 1", "Obligation 2"]
+ }},
+ "important_dates": ["Date 1", "Date 2"]
+}}"""
+
+ completion = groq_client.chat.completions.create(
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ messages=[
+ {
+ "role": "system",
+ "content": "You are an expert contract analyst. Create clear, concise summaries of contracts. Always respond with valid JSON only."
+ },
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.3,
+ max_completion_tokens=1200,
+ response_format={"type": "json_object"}
+ )
+
+ content = completion.choices[0].message.content if completion.choices else "{}"
+ content = content.strip()
+
+ # Clean JSON response
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.startswith("```"):
+ content = content[3:]
+ if content.endswith("```"):
+ content = content[:-3]
+ content = content.strip()
+
+ result = json.loads(content)
+
+ return ContractSummaryResponse(
+ summary=result.get("summary", "Contract summary unavailable."),
+ key_terms=result.get("key_terms", {}),
+ obligations=result.get("obligations", {"brand": [], "creator": []}),
+ important_dates=result.get("important_dates", [])
+ )
+
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Failed to parse AI response")
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error summarizing contract: {str(e)}"
+ ) from e
diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py
new file mode 100644
index 0000000..eebe93b
--- /dev/null
+++ b/backend/app/core/__init__.py
@@ -0,0 +1 @@
+# core package init
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
new file mode 100644
index 0000000..8370082
--- /dev/null
+++ b/backend/app/core/config.py
@@ -0,0 +1,40 @@
+from pydantic_settings import BaseSettings
+from typing import Optional, Dict, Any
+import json
+
+class Settings(BaseSettings):
+ # Supabase Configuration
+ supabase_url: str
+ supabase_key: str
+ supabase_service_key: str
+
+ # Database Configuration
+ database_url: Optional[str] = None
+
+ # AI Configuration
+ ai_api_key: Optional[str] = None
+ groq_api_key: Optional[str] = None
+ gemini_api_key: Optional[str] = None
+
+ # CORS Configuration
+ allowed_origins: str = "http://localhost:3000"
+
+ # Server Configuration
+ host: str = "0.0.0.0"
+ port: int = 8000
+
+ # Application Settings
+ app_name: Optional[str] = None
+
+ # JWT Authentication (RAW JSON STRING)
+ SUPABASE_JWT_PUBLIC_KEY: str
+
+ model_config = {
+ "env_file": ".env"
+ }
+
+ @property
+ def supabase_jwt_jwk(self) -> Dict[str, Any]:
+ return json.loads(self.SUPABASE_JWT_PUBLIC_KEY)
+
+settings = Settings()
diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py
new file mode 100644
index 0000000..e0ec7d6
--- /dev/null
+++ b/backend/app/core/dependencies.py
@@ -0,0 +1,217 @@
+"""
+FastAPI dependencies for authentication and authorization
+Used across all protected endpoints
+"""
+
+from typing import Optional
+from fastapi import Depends, HTTPException, status
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from app.core.security import jwt_handler
+from app.core.supabase_clients import supabase_anon
+
+
+# Security scheme for Swagger docs
+security = HTTPBearer(
+ scheme_name="JWT Bearer Token",
+ description="Enter your Supabase JWT token"
+)
+
+# Optional security scheme for endpoints that work with or without auth
+optional_security = HTTPBearer(
+ scheme_name="JWT Bearer Token (Optional)",
+ description="Enter your Supabase JWT token (optional)",
+ auto_error=False
+)
+
+
+async def get_current_user(
+ credentials: HTTPAuthorizationCredentials = Depends(security)
+) -> dict:
+ """
+ Dependency to get current authenticated user from JWT token
+
+ Usage:
+ @app.get("/protected")
+ async def protected_route(user = Depends(get_current_user)):
+ return {"user_id": user["id"]}
+
+ Returns:
+ User profile dict with id, email, role
+
+ Raises:
+ HTTPException 401: If token is invalid or user not found
+ """
+
+ # Extract token from Bearer scheme
+ token = credentials.credentials
+
+ # Decode and validate token
+ try:
+ payload = jwt_handler.decode_token(token)
+ except HTTPException as e:
+ raise e
+
+ # Get user ID from token
+ user_id = payload.get('sub')
+
+ if not user_id:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid token: no user ID"
+ )
+
+ # Fetch user profile from database
+ try:
+ response = supabase_anon.table('profiles') \
+ .select('*') \
+ .eq('id', user_id) \
+ .single() \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="User profile not found"
+ )
+
+ user = response.data
+
+ # Add token email if not in profile
+ if 'email' not in user:
+ user['email'] = payload.get('email')
+
+ return user
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error fetching user: {str(e)}"
+ )
+
+
+async def get_current_creator(
+ current_user: dict = Depends(get_current_user)
+) -> dict:
+ """
+ Dependency to verify user is a creator and get creator profile
+
+ Usage:
+ @app.get("/creator-only")
+ async def creator_route(creator = Depends(get_current_creator)):
+ return {"creator_id": creator["id"]}
+
+ Returns:
+ Creator profile dict
+
+ Raises:
+ HTTPException 403: If user is not a creator
+ """
+
+ if current_user.get('role') != 'Creator':
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="This endpoint is only accessible to creators"
+ )
+
+ # Fetch creator profile
+ try:
+ response = supabase_anon.table('creators') \
+ .select('*') \
+ .eq('user_id', current_user['id']) \
+ .single() \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Creator profile not found. Please complete onboarding."
+ )
+
+ return response.data
+ except Exception as e:
+ # Check if it's a "not found" error from Supabase
+ if "PGRST116" in str(e) or "No rows" in str(e):
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Creator profile not found. Please complete onboarding."
+ )
+ # Re-raise other exceptions
+ raise
+
+
+async def get_current_brand(
+ current_user: dict = Depends(get_current_user)
+) -> dict:
+ """
+ Dependency to verify user is a brand and get brand profile
+
+ Usage:
+ @app.get("/brand-only")
+ async def brand_route(brand = Depends(get_current_brand)):
+ return {"brand_id": brand["id"]}
+
+ Returns:
+ Brand profile dict
+
+ Raises:
+ HTTPException 403: If user is not a brand
+ """
+
+ if current_user.get('role') != 'Brand':
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="This endpoint is only accessible to brands"
+ )
+
+ # Fetch brand profile
+ try:
+ response = supabase_anon.table('brands') \
+ .select('*') \
+ .eq('user_id', current_user['id']) \
+ .single() \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Brand profile not found. Please complete onboarding."
+ )
+
+ return response.data
+ except HTTPException:
+ raise
+ except Exception as e:
+ # Check if it's a "not found" error from Supabase
+ if "PGRST116" in str(e) or "No rows" in str(e):
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Brand profile not found. Please complete onboarding."
+ ) from e
+ # Re-raise other exceptions
+ raise
+
+
+async def get_optional_user(
+ credentials: Optional[HTTPAuthorizationCredentials] = Depends(optional_security)
+) -> Optional[dict]:
+ """
+ Dependency for endpoints that work with or without authentication
+
+ Usage:
+ @app.get("/public-but-personalized")
+ async def route(user = Depends(get_optional_user)):
+ if user:
+ return {"message": f"Hello {user['name']}"}
+ return {"message": "Hello guest"}
+
+ Returns:
+ User profile dict if authenticated, None otherwise
+ """
+
+ if not credentials:
+ return None
+
+ try:
+ return await get_current_user(credentials)
+ except HTTPException:
+ return None
diff --git a/backend/app/core/security.py b/backend/app/core/security.py
new file mode 100644
index 0000000..93f3d85
--- /dev/null
+++ b/backend/app/core/security.py
@@ -0,0 +1,94 @@
+"""
+Security utilities for JWT authentication
+Handles token validation, creation, and user verification
+"""
+
+from jose import jwt, JWTError
+from fastapi import HTTPException, status
+from typing import Optional, Dict, Any
+from app.core.config import settings
+
+
+class JWTHandler:
+ """Handle Supabase JWT verification only"""
+
+ def __init__(self):
+ self.public_key = settings.SUPABASE_JWT_PUBLIC_KEY
+ if not self.public_key:
+ raise ValueError("SUPABASE_JWT_PUBLIC_KEY is not set")
+
+ self.algorithm = "ES256"
+
+ def decode_token(self, token: str) -> Dict[str, Any]:
+ try:
+ payload = jwt.decode(
+ token,
+ self.public_key, # ✅ PUBLIC KEY
+ algorithms=[self.algorithm],
+ audience="authenticated", # Supabase default
+ )
+
+ if "sub" not in payload:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid token: missing user id",
+ )
+
+ return payload
+
+ except JWTError as e:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail=f"Invalid token: {str(e)}",
+ )
+
+ def verify_token(self, token: str) -> bool:
+ try:
+ jwt.decode(
+ token,
+ self.public_key,
+ algorithms=[self.algorithm],
+ audience="authenticated",
+ )
+ return True
+ except JWTError:
+ return False
+
+ def get_user_id_from_token(self, token: str) -> str:
+ return self.decode_token(token)["sub"]
+
+ def get_user_email_from_token(self, token: str) -> Optional[str]:
+ return self.decode_token(token).get("email")
+
+ def get_user_role_from_token(self, token: str) -> Optional[str]:
+ payload = self.decode_token(token)
+ return (
+ payload.get("user_metadata", {}).get("role")
+ or payload.get("app_metadata", {}).get("role")
+ or payload.get("role")
+ )
+
+
+jwt_handler = JWTHandler()
+
+
+
+# Legacy functions for backward compatibility (optional - requires passlib)
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ """Verify a password against a hash (if needed for custom auth)"""
+ try:
+ from passlib.context import CryptContext
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+ return pwd_context.verify(plain_password, hashed_password)
+ except ImportError:
+ raise ImportError("passlib is required for password verification. Install with: pip install passlib[bcrypt]")
+
+
+def get_password_hash(password: str) -> str:
+ """Hash a password (if needed for custom auth)"""
+ try:
+ from passlib.context import CryptContext
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+ return pwd_context.hash(password)
+ except ImportError:
+ raise ImportError("passlib is required for password hashing. Install with: pip install passlib[bcrypt]")
diff --git a/backend/app/core/supabase_clients.py b/backend/app/core/supabase_clients.py
new file mode 100644
index 0000000..4e84c40
--- /dev/null
+++ b/backend/app/core/supabase_clients.py
@@ -0,0 +1,14 @@
+"""
+Supabase client instances for different use cases:
+- supabase_anon: For user-facing operations (anon key)
+- supabase_admin: For server-side atomic operations (service role)
+"""
+
+from supabase import create_client
+from app.core.config import settings
+
+# Client for user-facing operations (anon key)
+supabase_anon = create_client(settings.supabase_url, settings.supabase_key)
+
+# Admin client for server-side atomic operations (service role)
+supabase_admin = create_client(settings.supabase_url, settings.supabase_service_key)
diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py
new file mode 100644
index 0000000..691cef1
--- /dev/null
+++ b/backend/app/db/__init__.py
@@ -0,0 +1 @@
+# db package init
diff --git a/backend/app/db/database.py b/backend/app/db/database.py
new file mode 100644
index 0000000..f55fa7c
--- /dev/null
+++ b/backend/app/db/database.py
@@ -0,0 +1 @@
+# Database connection setup
diff --git a/backend/app/main.py b/backend/app/main.py
new file mode 100644
index 0000000..f05d402
--- /dev/null
+++ b/backend/app/main.py
@@ -0,0 +1,46 @@
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from app.api.routes import (
+ health,
+ auth,
+ gemini_generate,
+ campaigns,
+ groq_generate,
+ collaborations,
+ creators,
+ proposals,
+ analytics,
+ ai_analytics,
+ profiles,
+)
+
+app = FastAPI(title="Inpact Backend", version="0.1.0")
+
+# 🔥 FORCE OPEN CORS (DEBUG MODE)
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=False, # must be False when using "*"
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+# Routes
+app.include_router(health.router)
+app.include_router(auth.router)
+app.include_router(gemini_generate.router)
+app.include_router(campaigns.router)
+app.include_router(groq_generate.router)
+app.include_router(collaborations.router)
+app.include_router(creators.router)
+app.include_router(proposals.router)
+app.include_router(analytics.router)
+app.include_router(ai_analytics.router)
+app.include_router(profiles.router)
+
+@app.get("/")
+def root():
+ return {"message": "Inpact Backend Running 🚀"}
+
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
new file mode 100644
index 0000000..654e47b
--- /dev/null
+++ b/backend/app/models/__init__.py
@@ -0,0 +1 @@
+# models package init
diff --git a/backend/app/models/token.py b/backend/app/models/token.py
new file mode 100644
index 0000000..426b0b6
--- /dev/null
+++ b/backend/app/models/token.py
@@ -0,0 +1,26 @@
+"""
+Pydantic models for authentication tokens
+"""
+
+from pydantic import BaseModel
+from typing import Optional
+
+
+class Token(BaseModel):
+ """JWT access token response"""
+ access_token: str
+ token_type: str = "bearer"
+ expires_in: int # seconds
+
+
+class TokenData(BaseModel):
+ """Decoded token data"""
+ user_id: str
+ email: Optional[str] = None
+ role: Optional[str] = None
+
+
+class TokenRefresh(BaseModel):
+ """Token refresh request"""
+ refresh_token: str
+
diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py
new file mode 100644
index 0000000..824534c
--- /dev/null
+++ b/backend/app/services/__init__.py
@@ -0,0 +1,3 @@
+"""
+Services module for backend application
+"""
diff --git a/backend/app/services/supabase_client.py b/backend/app/services/supabase_client.py
new file mode 100644
index 0000000..d1078b2
--- /dev/null
+++ b/backend/app/services/supabase_client.py
@@ -0,0 +1,5 @@
+from supabase import create_client, Client
+from app.core.config import settings
+
+# Initialize Supabase client with anon key (public)
+supabase: Client = create_client(settings.supabase_url, settings.supabase_key)
diff --git a/backend/env_example b/backend/env_example
new file mode 100644
index 0000000..1d6ad33
--- /dev/null
+++ b/backend/env_example
@@ -0,0 +1,36 @@
+# Example environment file for backend
+
+# Supabase Configuration (Required)
+
+# Get these from: https://app.supabase.com/project/_/settings/api
+
+SUPABASE_URL=https://your-project.supabase.co
+SUPABASE_KEY=your-supabase-anon-key-here
+SUPABASE_SERVICE_KEY=your-service-role-key
+
+# Database Configuration (Optional - Supabase PostgreSQL direct connection)
+
+# Get this from: Settings → Database → Connection string → URI
+
+DATABASE_URL=postgresql://postgres.your-project-ref:[YOUR-PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres
+
+# AI Configuration (Optional)
+
+GROQ_API_KEY=your-groq-api-key
+AI_API_KEY=your-openai-api-key-optional
+
+# Gemini API Key (Optional)
+
+GEMINI_API_KEY=your-gemini-api-key-here
+
+# CORS Origins (comma-separated)
+# For production, include your production frontend URL(s)
+# Example: ALLOWED_ORIGINS=https://your-frontend-domain.com,https://www.your-frontend-domain.com
+
+ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
+
+# JWT Secret Key from Supabase
+# Location: Dashboard → Project Settings → API → JWT Settings → JWT Secret
+# Use the JWT Secret (NOT the anon key!)
+
+SUPABASE_JWT_SECRET=your_jwt_key
diff --git a/backend/list_endpoints.py b/backend/list_endpoints.py
new file mode 100644
index 0000000..6c319b5
--- /dev/null
+++ b/backend/list_endpoints.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+"""
+Script to list all API endpoints from the FastAPI application
+"""
+import sys
+import os
+from pathlib import Path
+
+# Add the backend directory to the path
+backend_dir = Path(__file__).parent
+sys.path.insert(0, str(backend_dir))
+
+from app.main import app
+
+def list_endpoints():
+ """List all registered routes in the FastAPI app"""
+ print("=" * 80)
+ print("INPACT AI - API ENDPOINTS")
+ print("=" * 80)
+ print()
+
+ # Group endpoints by method
+ endpoints_by_method = {}
+
+ for route in app.routes:
+ if hasattr(route, 'methods') and hasattr(route, 'path'):
+ methods = list(route.methods)
+ path = route.path
+
+ for method in methods:
+ if method not in endpoints_by_method:
+ endpoints_by_method[method] = []
+ endpoints_by_method[method].append(path)
+
+ # Print endpoints grouped by HTTP method
+ for method in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
+ if method in endpoints_by_method:
+ print(f"\n{method} Endpoints:")
+ print("-" * 80)
+ for path in sorted(set(endpoints_by_method[method])):
+ print(f" {method:6} {path}")
+
+ print("\n" + "=" * 80)
+ print(f"Total unique endpoints: {len(set([route.path for route in app.routes if hasattr(route, 'path')]))}")
+ print("=" * 80)
+ print()
+ print("💡 TIP: Visit /docs for interactive API documentation")
+ print("💡 TIP: Visit /redoc for alternative API documentation")
+
+if __name__ == "__main__":
+ list_endpoints()
+
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 0000000..f1e496a
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,33 @@
+annotated-doc==0.0.3
+annotated-types==0.7.0
+anyio==4.11.0
+black==25.9.0
+click==8.3.0
+email-validator==2.3.0
+fastapi==0.120.3
+google-generativeai==0.8.5
+gotrue==2.12.4
+groq==0.33.0
+h11==0.16.0
+httpx==0.28.1
+idna==3.11
+mypy_extensions==1.1.0
+packaging==25.0
+passlib[bcrypt]==1.7.4
+pathspec==0.12.1
+platformdirs==4.5.0
+postgrest==2.23.0
+pydantic==2.12.3
+pydantic-settings==2.11.0
+pydantic_core==2.41.4
+PyJWT[crypto]==2.10.1
+python-dotenv==1.2.1
+python-multipart==0.0.20
+requests==2.32.5
+ruff==0.14.3
+sniffio==1.3.1
+starlette==0.49.1
+supabase==2.23.0
+typing-inspection==0.4.2
+typing_extensions==4.15.0
+uvicorn==0.38.0
diff --git a/backend/test_jwt.py b/backend/test_jwt.py
new file mode 100644
index 0000000..d2d34e6
--- /dev/null
+++ b/backend/test_jwt.py
@@ -0,0 +1,113 @@
+"""
+Test script to verify JWT authentication
+Run: python test_jwt.py
+"""
+
+import requests
+import os
+from dotenv import load_dotenv
+from jose import jwt, JWTError
+
+load_dotenv()
+
+# Configuration
+API_URL = "http://localhost:8000"
+SUPABASE_URL = os.getenv("SUPABASE_URL")
+SUPABASE_ANON_KEY = os.getenv("SUPABASE_KEY")
+
+# Test user credentials (update with your test account)
+TEST_EMAIL = "anu906162@gmail.com"
+TEST_PASSWORD = "@rani00@"
+# @rani00@
+
+
+
+def get_jwt_token():
+ """Login and get JWT token from Supabase"""
+
+ # Using Supabase Auth API directly
+ response = requests.post(
+ f"{SUPABASE_URL}/auth/v1/token?grant_type=password",
+ headers={
+ "apikey": SUPABASE_ANON_KEY,
+ "Content-Type": "application/json"
+ },
+ json={
+ "email": TEST_EMAIL,
+ "password": TEST_PASSWORD
+ },
+ timeout=10
+ )
+
+ if response.status_code != 200:
+ print(f"Login failed: {response.text}")
+ return None
+
+ data = response.json()
+ return data.get("access_token")
+
+
+def test_protected_endpoint(token):
+ """Test accessing protected endpoint with JWT"""
+
+ response = requests.get(
+ f"{API_URL}/campaigns",
+ headers={
+ "Authorization": f"Bearer {token}"
+ },
+ timeout=10
+ )
+
+ print(f"Status: {response.status_code}")
+ print(f"Response: {response.json()}")
+
+ return response.status_code == 200
+
+
+def test_invalid_token():
+ """Test with invalid token (should fail)"""
+
+ response = requests.get(
+ f"{API_URL}/campaigns",
+ headers={
+ "Authorization": "Bearer invalid_token_here"
+ },
+ timeout=10
+ )
+
+ print(f"Invalid token status: {response.status_code}")
+ print(f"Error message: {response.json()}")
+
+ return response.status_code == 401
+
+
+if __name__ == "__main__":
+ print("=== JWT Authentication Test ===\n")
+
+ # Test 1: Get JWT token
+ print("1. Getting JWT token...")
+ token = get_jwt_token()
+
+ if not token:
+ print("❌ Failed to get JWT token")
+ exit(1)
+
+ print(f"✅ Got JWT token: {token[:50]}...\n")
+
+ # Test 2: Access protected endpoint
+ print("2. Testing protected endpoint with valid token...")
+ if test_protected_endpoint(token):
+ print("✅ Successfully accessed protected endpoint\n")
+ else:
+ print("❌ Failed to access protected endpoint\n")
+
+ # Test 3: Test invalid token
+ print("3. Testing with invalid token (should fail)...")
+ if test_invalid_token():
+ print("✅ Correctly rejected invalid token\n")
+ else:
+ print("❌ Invalid token was accepted (security issue!)\n")
+
+ print("=== Tests Complete ===")
+
+
diff --git a/docs/PROPOSALS_IMPLEMENTATION_SUMMARY.md b/docs/PROPOSALS_IMPLEMENTATION_SUMMARY.md
new file mode 100644
index 0000000..b042fc1
--- /dev/null
+++ b/docs/PROPOSALS_IMPLEMENTATION_SUMMARY.md
@@ -0,0 +1,262 @@
+# Proposals & Find Creators Feature Implementation Summary
+
+## Overview
+
+This document summarizes the implementation of the Find Creators and Proposals features for the InPactAI platform. These features allow brands to find matching creators using AI and send collaboration proposals.
+
+## Features Implemented
+
+### 1. Find Creators Feature
+
+**Location**: `/brand/campaigns/[campaign_id]/find-creators`
+
+**Functionality**:
+- AI-powered creator matching using Groq LLM
+- Displays top 4 matching creators based on campaign requirements and brand profile
+- Shows match scores and reasoning for each creator
+- Expandable creator details view
+- Direct proposal sending from the find creators page
+
+**Key Components**:
+- Backend endpoint: `GET /campaigns/{campaign_id}/find-creators`
+- Frontend page: `frontend/app/brand/campaigns/[campaign_id]/find-creators/page.tsx`
+- Uses rule-based scoring + AI refinement for better matches
+
+### 2. Proposals System
+
+**Functionality**:
+- Brands can send proposals to creators
+- Creators can view and respond to proposals (accept/decline)
+- AI-powered proposal content drafting
+- Status tracking (pending, accepted, declined, withdrawn)
+
+**Key Components**:
+- Backend endpoints in `backend/app/api/routes/proposals.py`
+- Brand proposals page: `frontend/app/brand/proposals/page.tsx`
+- Creator proposals page: `frontend/app/creator/proposals/page.tsx`
+- Sliding menu updated with Proposals link
+
+### 3. AI Proposal Drafting
+
+**Endpoint**: `POST /proposals/draft`
+
+**Functionality**:
+- Uses Groq LLM to draft professional proposal content
+- Considers brand profile, campaign details, and creator profile
+- Generates subject line and message body
+- Takes into account content ideas and ideal pricing
+
+## Database Schema
+
+### New Table: `proposals`
+
+**Location**: Added to `backend/SQL` file
+
+**Columns**:
+- `id` (uuid, primary key)
+- `campaign_id` (uuid, foreign key → campaigns)
+- `brand_id` (uuid, foreign key → brands)
+- `creator_id` (uuid, foreign key → creators)
+- `subject` (text, required)
+- `message` (text, required)
+- `proposed_amount` (numeric, optional)
+- `content_ideas` (text[], optional)
+- `ideal_pricing` (text, optional)
+- `status` (text, default: 'pending', check: pending|accepted|declined|withdrawn)
+- `created_at` (timestamp)
+- `updated_at` (timestamp)
+
+**Constraints**:
+- Unique constraint on `(campaign_id, creator_id, brand_id)` to prevent duplicates
+- Foreign keys with CASCADE delete
+- Check constraint on status field
+
+**Indexes**:
+- `idx_proposals_campaign_id`
+- `idx_proposals_brand_id`
+- `idx_proposals_creator_id`
+- `idx_proposals_status`
+- `idx_proposals_created_at`
+
+**Triggers**:
+- `update_proposals_updated_at` - Auto-updates `updated_at` on row changes
+
+## Supabase Setup Instructions
+
+### 1. Create the Proposals Table
+
+Run the following SQL in your Supabase SQL Editor:
+
+```sql
+-- Proposals table for brand-to-creator collaboration proposals
+CREATE TABLE IF NOT EXISTS public.proposals (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ campaign_id uuid NOT NULL,
+ brand_id uuid NOT NULL,
+ creator_id uuid NOT NULL,
+ subject text NOT NULL,
+ message text NOT NULL,
+ proposed_amount numeric,
+ content_ideas text[] DEFAULT ARRAY[]::text[],
+ ideal_pricing text,
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined', 'withdrawn')),
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT proposals_pkey PRIMARY KEY (id),
+ CONSTRAINT proposals_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id) ON DELETE CASCADE,
+ CONSTRAINT proposals_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id) ON DELETE CASCADE,
+ CONSTRAINT proposals_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id) ON DELETE CASCADE,
+ CONSTRAINT proposals_unique_campaign_creator UNIQUE (campaign_id, creator_id, brand_id)
+);
+
+-- Create indexes
+CREATE INDEX IF NOT EXISTS idx_proposals_campaign_id ON public.proposals(campaign_id);
+CREATE INDEX IF NOT EXISTS idx_proposals_brand_id ON public.proposals(brand_id);
+CREATE INDEX IF NOT EXISTS idx_proposals_creator_id ON public.proposals(creator_id);
+CREATE INDEX IF NOT EXISTS idx_proposals_status ON public.proposals(status);
+CREATE INDEX IF NOT EXISTS idx_proposals_created_at ON public.proposals(created_at DESC);
+
+-- Add updated_at trigger
+CREATE OR REPLACE FUNCTION update_proposals_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = now();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER update_proposals_updated_at
+ BEFORE UPDATE ON public.proposals
+ FOR EACH ROW
+ EXECUTE FUNCTION update_proposals_updated_at();
+```
+
+### 2. Set Up Row Level Security (RLS)
+
+Add RLS policies for the proposals table:
+
+```sql
+-- Enable RLS
+ALTER TABLE public.proposals ENABLE ROW LEVEL SECURITY;
+
+-- Policy: Brands can view their own sent proposals
+CREATE POLICY "Brands can view their sent proposals"
+ ON public.proposals FOR SELECT
+ USING (
+ EXISTS (
+ SELECT 1 FROM public.brands
+ WHERE brands.id = proposals.brand_id
+ AND brands.user_id = auth.uid()
+ )
+ );
+
+-- Policy: Creators can view their received proposals
+CREATE POLICY "Creators can view their received proposals"
+ ON public.proposals FOR SELECT
+ USING (
+ EXISTS (
+ SELECT 1 FROM public.creators
+ WHERE creators.id = proposals.creator_id
+ AND creators.user_id = auth.uid()
+ )
+ );
+
+-- Policy: Brands can create proposals
+CREATE POLICY "Brands can create proposals"
+ ON public.proposals FOR INSERT
+ WITH CHECK (
+ EXISTS (
+ SELECT 1 FROM public.brands
+ WHERE brands.id = proposals.brand_id
+ AND brands.user_id = auth.uid()
+ )
+ );
+
+-- Policy: Brands can update their own proposals (to withdraw)
+CREATE POLICY "Brands can update their proposals"
+ ON public.proposals FOR UPDATE
+ USING (
+ EXISTS (
+ SELECT 1 FROM public.brands
+ WHERE brands.id = proposals.brand_id
+ AND brands.user_id = auth.uid()
+ )
+ );
+
+-- Policy: Creators can update proposals they received (to accept/decline)
+CREATE POLICY "Creators can update received proposals"
+ ON public.proposals FOR UPDATE
+ USING (
+ EXISTS (
+ SELECT 1 FROM public.creators
+ WHERE creators.id = proposals.creator_id
+ AND creators.user_id = auth.uid()
+ )
+ );
+```
+
+## API Endpoints
+
+### Find Creators
+- `GET /campaigns/{campaign_id}/find-creators?limit=4&use_ai=true`
+ - Returns top matching creators for a campaign
+ - Uses AI (Groq) to refine matches and generate reasoning
+
+### Proposals
+- `POST /proposals` - Create a new proposal
+- `GET /proposals/sent` - Get proposals sent by current brand
+- `GET /proposals/received` - Get proposals received by current creator
+- `PUT /proposals/{proposal_id}/status` - Update proposal status
+- `POST /proposals/draft?campaign_id={id}&creator_id={id}` - AI-draft proposal content
+
+## Frontend Routes
+
+### Brand Routes
+- `/brand/campaigns` - Campaigns list (now includes "Find Creators" button)
+- `/brand/campaigns/[campaign_id]/find-creators` - Find matching creators page
+- `/brand/proposals` - View sent proposals
+
+### Creator Routes
+- `/creator/proposals` - View received proposals
+
+## UI Components
+
+### Updated Components
+- `SlidingMenu.tsx` - Added Proposals link for both brands and creators
+- `frontend/app/brand/campaigns/page.tsx` - Added "Find Creators" button
+
+### New Components
+- `frontend/app/brand/campaigns/[campaign_id]/find-creators/page.tsx`
+- `frontend/app/brand/proposals/page.tsx`
+- `frontend/app/creator/proposals/page.tsx`
+
+## Configuration Required
+
+### Environment Variables
+Ensure these are set in your backend `.env`:
+- `GROQ_API_KEY` - Required for AI matching and proposal drafting
+
+### Backend Dependencies
+The following Python packages are used (should already be in requirements.txt):
+- `groq` - For AI-powered matching and content generation
+
+## Testing Checklist
+
+- [ ] Create proposals table in Supabase
+- [ ] Set up RLS policies
+- [ ] Test finding creators for a campaign
+- [ ] Test sending a proposal
+- [ ] Test AI proposal drafting
+- [ ] Test viewing sent proposals (brand)
+- [ ] Test viewing received proposals (creator)
+- [ ] Test accepting a proposal (creator)
+- [ ] Test declining a proposal (creator)
+- [ ] Test withdrawing a proposal (brand)
+
+## Notes
+
+- The Find Creators feature uses a hybrid approach: rule-based initial scoring + AI refinement
+- Proposals prevent duplicates via unique constraint on (campaign_id, creator_id, brand_id)
+- All proposal operations respect RLS policies for security
+- The AI drafting feature requires Groq API key to be configured
+
diff --git a/docs/database/PROPOSALS_SCHEMA.md b/docs/database/PROPOSALS_SCHEMA.md
new file mode 100644
index 0000000..89e8908
--- /dev/null
+++ b/docs/database/PROPOSALS_SCHEMA.md
@@ -0,0 +1,122 @@
+# Proposals Table Schema
+
+## Overview
+
+The `proposals` table stores collaboration proposals sent by brands to creators for specific campaigns. This enables brands to proactively reach out to creators they've identified as good matches for their campaigns.
+
+## Table Structure
+
+### `proposals`
+
+| Column | Type | Constraints | Description |
+|--------|------|-------------|-------------|
+| `id` | uuid | PRIMARY KEY, DEFAULT uuid_generate_v4() | Unique proposal identifier |
+| `campaign_id` | uuid | NOT NULL, FOREIGN KEY → campaigns(id) | Reference to the campaign |
+| `brand_id` | uuid | NOT NULL, FOREIGN KEY → brands(id) | Reference to the brand sending the proposal |
+| `creator_id` | uuid | NOT NULL, FOREIGN KEY → creators(id) | Reference to the creator receiving the proposal |
+| `subject` | text | NOT NULL | Subject line of the proposal |
+| `message` | text | NOT NULL | Full proposal message content |
+| `proposed_amount` | numeric | NULL | Proposed compensation amount in INR |
+| `content_ideas` | text[] | DEFAULT [] | Array of content ideas suggested |
+| `ideal_pricing` | text | NULL | Text description of ideal pricing |
+| `status` | text | NOT NULL, DEFAULT 'pending', CHECK | Status: 'pending', 'accepted', 'declined', 'withdrawn' |
+| `created_at` | timestamp with time zone | DEFAULT now() | When the proposal was created |
+| `updated_at` | timestamp with time zone | DEFAULT now() | When the proposal was last updated |
+
+### Constraints
+
+- **Primary Key**: `id`
+- **Foreign Keys**:
+ - `campaign_id` → `campaigns(id)` ON DELETE CASCADE
+ - `brand_id` → `brands(id)` ON DELETE CASCADE
+ - `creator_id` → `creators(id)` ON DELETE CASCADE
+- **Unique Constraint**: `(campaign_id, creator_id, brand_id)` - Prevents duplicate proposals for the same campaign-creator-brand combination
+- **Check Constraint**: `status` must be one of: 'pending', 'accepted', 'declined', 'withdrawn'
+
+### Indexes
+
+- `idx_proposals_campaign_id` on `campaign_id`
+- `idx_proposals_brand_id` on `brand_id`
+- `idx_proposals_creator_id` on `creator_id`
+- `idx_proposals_status` on `status`
+- `idx_proposals_created_at` on `created_at DESC`
+
+### Triggers
+
+- `update_proposals_updated_at`: Automatically updates `updated_at` timestamp on row update
+
+## Status Flow
+
+1. **pending**: Initial status when proposal is created
+2. **accepted**: Creator accepts the proposal
+3. **declined**: Creator declines the proposal
+4. **withdrawn**: Brand withdraws the proposal
+
+## Usage Examples
+
+### Create a Proposal
+
+```sql
+INSERT INTO proposals (campaign_id, brand_id, creator_id, subject, message, proposed_amount)
+VALUES (
+ 'campaign-uuid',
+ 'brand-uuid',
+ 'creator-uuid',
+ 'Collaboration Opportunity',
+ 'We would love to work with you...',
+ 50000
+);
+```
+
+### Get Proposals Sent by a Brand
+
+```sql
+SELECT p.*, c.title as campaign_title, cr.display_name as creator_name
+FROM proposals p
+JOIN campaigns c ON p.campaign_id = c.id
+JOIN creators cr ON p.creator_id = cr.id
+WHERE p.brand_id = 'brand-uuid'
+ORDER BY p.created_at DESC;
+```
+
+### Get Proposals Received by a Creator
+
+```sql
+SELECT p.*, c.title as campaign_title, b.company_name as brand_name
+FROM proposals p
+JOIN campaigns c ON p.campaign_id = c.id
+JOIN brands b ON p.brand_id = b.id
+WHERE p.creator_id = 'creator-uuid'
+ORDER BY p.created_at DESC;
+```
+
+### Update Proposal Status
+
+```sql
+UPDATE proposals
+SET status = 'accepted', updated_at = now()
+WHERE id = 'proposal-uuid';
+```
+
+## Related Tables
+
+- **campaigns**: The campaign this proposal is for
+- **brands**: The brand sending the proposal
+- **creators**: The creator receiving the proposal
+
+## API Endpoints
+
+The proposals functionality is exposed through the following API endpoints:
+
+- `POST /proposals` - Create a new proposal
+- `GET /proposals/sent` - Get proposals sent by the current brand
+- `GET /proposals/received` - Get proposals received by the current creator
+- `PUT /proposals/{proposal_id}/status` - Update proposal status
+- `POST /proposals/draft` - AI-draft proposal content
+
+## Notes
+
+- The unique constraint on `(campaign_id, creator_id, brand_id)` ensures a brand can only send one proposal per creator per campaign
+- Proposals are automatically deleted when the associated campaign, brand, or creator is deleted (CASCADE)
+- The `updated_at` field is automatically maintained by a trigger
+
diff --git a/docs/database/schema-reference.md b/docs/database/schema-reference.md
new file mode 100644
index 0000000..237c4ea
--- /dev/null
+++ b/docs/database/schema-reference.md
@@ -0,0 +1,126 @@
+# Supabase Database Schema Reference
+
+> **Note:** This file is for reference/documentation only. It is not intended to be run as a migration or executed directly. The SQL below is a snapshot of the schema as exported from Supabase for context and developer understanding.
+
+---
+
+## About
+
+This file contains the exported DDL (Data Definition Language) statements for the database schema used in this project. It is provided for documentation and onboarding purposes. For actual migrations and schema changes, use the project's migration tool and scripts.
+
+---
+
+## Schema
+
+```sql
+-- Table for user profiles
+create table if not exists profiles (
+ id uuid references auth.users(id) on delete cascade,
+ name text not null,
+ role text check (role in ('Creator', 'Brand')) not null,
+ created_at timestamp with time zone default timezone('utc', now()),
+ primary key (id)
+);
+
+-- Table: brands
+CREATE TABLE public.brands (
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
+ user_id uuid NOT NULL UNIQUE,
+ company_name text NOT NULL,
+ company_tagline text,
+ company_description text,
+ company_logo_url text,
+ company_cover_image_url text,
+ industry text NOT NULL,
+ sub_industry text[] DEFAULT ARRAY[]::text[],
+ company_size text,
+ founded_year integer,
+ headquarters_location text,
+ company_type text,
+ website_url text NOT NULL,
+ contact_email text,
+ contact_phone text,
+ social_media_links jsonb,
+ target_audience_age_groups text[] DEFAULT ARRAY[]::text[],
+ target_audience_gender text[] DEFAULT ARRAY[]::text[],
+ target_audience_locations text[] DEFAULT ARRAY[]::text[],
+ target_audience_interests text[] DEFAULT ARRAY[]::text[],
+ target_audience_income_level text[] DEFAULT ARRAY[]::text[],
+ target_audience_description text,
+ brand_values text[] DEFAULT ARRAY[]::text[],
+ brand_personality text[] DEFAULT ARRAY[]::text[],
+ brand_voice text,
+ brand_colors jsonb,
+ marketing_goals text[] DEFAULT ARRAY[]::text[],
+ campaign_types_interested text[] DEFAULT ARRAY[]::text[],
+ preferred_content_types text[] DEFAULT ARRAY[]::text[],
+ preferred_platforms text[] DEFAULT ARRAY[]::text[],
+ campaign_frequency text,
+ monthly_marketing_budget numeric,
+ influencer_budget_percentage double precision,
+ budget_per_campaign_min numeric,
+ budget_per_campaign_max numeric,
+ typical_deal_size numeric,
+ payment_terms text,
+ offers_product_only_deals boolean DEFAULT false,
+ offers_affiliate_programs boolean DEFAULT false,
+ affiliate_commission_rate double precision,
+ preferred_creator_niches text[] DEFAULT ARRAY[]::text[],
+ preferred_creator_size text[] DEFAULT ARRAY[]::text[],
+ preferred_creator_locations text[] DEFAULT ARRAY[]::text[],
+ minimum_followers_required integer,
+ minimum_engagement_rate double precision,
+ content_dos text[] DEFAULT ARRAY[]::text[],
+ content_donts text[] DEFAULT ARRAY[]::text[],
+ brand_safety_requirements text[] DEFAULT ARRAY[]::text[],
+ competitor_brands text[] DEFAULT ARRAY[]::text[],
+ exclusivity_required boolean DEFAULT false,
+ exclusivity_duration_months integer,
+ past_campaigns_count integer DEFAULT 0,
+ successful_partnerships text[] DEFAULT ARRAY[]::text[],
+ case_studies text[] DEFAULT ARRAY[]::text[],
+ average_campaign_roi double precision,
+ products_services text[] DEFAULT ARRAY[]::text[],
+ product_price_range text,
+ product_categories text[] DEFAULT ARRAY[]::text[],
+ seasonal_products boolean DEFAULT false,
+ product_catalog_url text,
+ business_verified boolean DEFAULT false,
+ payment_verified boolean DEFAULT false,
+ tax_id_verified boolean DEFAULT false,
+ profile_completion_percentage integer DEFAULT 0,
+ is_active boolean DEFAULT true,
+ is_featured boolean DEFAULT false,
+ is_verified_brand boolean DEFAULT false,
+ subscription_tier text DEFAULT 'free'::text,
+ featured_until timestamp with time zone,
+ ai_profile_summary text,
+ search_keywords text[] DEFAULT ARRAY[]::text[],
+ matching_score_base double precision DEFAULT 50.0,
+ total_deals_posted integer DEFAULT 0,
+ total_deals_completed integer DEFAULT 0,
+ total_spent numeric DEFAULT 0,
+ average_deal_rating double precision,
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now(),
+ last_active_at timestamp with time zone DEFAULT now(),
+ CONSTRAINT brands_pkey PRIMARY KEY (id),
+ CONSTRAINT brands_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id)
+);
+
+-- ...existing code for other tables...
+```
+
+---
+
+## How to Use
+
+- Use this file for reference only. Do not run directly against your database.
+- For schema changes, use the migration scripts and tools defined in this project.
+- If you need to restore or migrate, use the official migration pipeline or tools.
+
+---
+
+## Source
+
+This schema was exported from Supabase using the "Save as SQL" feature for developer context.
diff --git a/frontend/.env.example b/frontend/.env.example
new file mode 100644
index 0000000..35d1b06
--- /dev/null
+++ b/frontend/.env.example
@@ -0,0 +1,6 @@
+NEXT_PUBLIC_API_URL=http://localhost:8000
+NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your-key-here
+NEXT_PUBLIC_APP_NAME=InPact AI
+NEXT_PUBLIC_APP_URL=http://localhost:3000
+
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..5ef6a52
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
new file mode 100644
index 0000000..024ca81
--- /dev/null
+++ b/frontend/.prettierignore
@@ -0,0 +1,26 @@
+# Ignore build output
+.next/
+dist/
+build/
+
+# Ignore dependencies
+node_modules/
+
+# Ignore environment files
+.env*
+
+# Ignore lock files
+package-lock.json
+yarn.lock
+pnpm-lock.yaml
+
+# Ignore coverage and logs
+coverage/
+*.log
+
+# Ignore public assets
+public/
+
+# Ignore other config files
+.eslint*
+# ...existing code...
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
new file mode 100644
index 0000000..b34596e
--- /dev/null
+++ b/frontend/.prettierrc
@@ -0,0 +1,13 @@
+{
+ "semi": true,
+ "trailingComma": "es5",
+ "singleQuote": false,
+ "printWidth": 80,
+ "tabWidth": 2,
+ "useTabs": false,
+ "arrowParens": "always",
+ "endOfLine": "lf",
+ "bracketSpacing": true,
+ "jsxSingleQuote": false,
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
\ No newline at end of file
diff --git a/Backend/app/config.py b/frontend/AUTH_README.md
similarity index 100%
rename from Backend/app/config.py
rename to frontend/AUTH_README.md
diff --git a/frontend/BACKEND_ENV_SETUP.md b/frontend/BACKEND_ENV_SETUP.md
new file mode 100644
index 0000000..0b4e7bc
--- /dev/null
+++ b/frontend/BACKEND_ENV_SETUP.md
@@ -0,0 +1,32 @@
+# Deployment Environment Configuration
+
+## Environment Variables
+
+- `NEXT_PUBLIC_API_URL` must be set to a valid HTTPS URL for the backend API.
+- Example for production:
+ - `NEXT_PUBLIC_API_URL=https://your-production-backend.example.com`
+
+## HTTPS Enforcement
+
+- All backend URLs must use HTTPS in production.
+- The application will fail to start if `NEXT_PUBLIC_API_URL` is missing or not HTTPS.
+
+## Vercel Deployment
+
+- See `vercel.json` for environment and rewrite configuration.
+
+## Docker Deployment (optional)
+
+If deploying with Docker, ensure the environment variable is set in your Dockerfile or deployment environment:
+
+```
+ENV NEXT_PUBLIC_API_URL=https://your-production-backend.example.com
+```
+
+## Local Development
+
+- For local development, you may use HTTP, but production must use HTTPS.
+
+---
+
+**Do not deploy to production without setting a valid HTTPS backend URL.**
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..e215bc4
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/Backend/app/models/posts.py b/frontend/UI_UX_GUIDE.md
similarity index 100%
rename from Backend/app/models/posts.py
rename to frontend/UI_UX_GUIDE.md
diff --git a/frontend/app/brand/analytics/page.tsx b/frontend/app/brand/analytics/page.tsx
new file mode 100644
index 0000000..17ee7e1
--- /dev/null
+++ b/frontend/app/brand/analytics/page.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import BrandAnalyticsDashboard from "@/components/analytics/BrandAnalyticsDashboard";
+import AIAnalyticsDashboard from "@/components/analytics/AIAnalyticsDashboard";
+import { useState } from "react";
+
+export default function BrandAnalyticsPage() {
+ const [activeView, setActiveView] = useState<"standard" | "ai">("ai");
+
+ return (
+
+
+
+
+
+ setActiveView("ai")}
+ className={`px-4 py-2 rounded-lg font-medium ${
+ activeView === "ai"
+ ? "bg-purple-600 text-white"
+ : "bg-white text-gray-700 hover:bg-gray-50"
+ }`}
+ >
+ AI Analytics
+
+ setActiveView("standard")}
+ className={`px-4 py-2 rounded-lg font-medium ${
+ activeView === "standard"
+ ? "bg-purple-600 text-white"
+ : "bg-white text-gray-700 hover:bg-gray-50"
+ }`}
+ >
+ Standard Analytics
+
+
+ {activeView === "ai" ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/app/brand/campaigns/[campaign_id]/find-creators/page.tsx b/frontend/app/brand/campaigns/[campaign_id]/find-creators/page.tsx
new file mode 100644
index 0000000..3ab36d8
--- /dev/null
+++ b/frontend/app/brand/campaigns/[campaign_id]/find-creators/page.tsx
@@ -0,0 +1,936 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import { authenticatedFetch } from "@/lib/auth-helpers";
+import { fetchCampaignById } from "@/lib/campaignApi";
+import { Campaign } from "@/types/campaign";
+import {
+ ChevronDown,
+ ChevronUp,
+ Loader2,
+ Search,
+ Sparkles,
+ User,
+ Users,
+ TrendingUp,
+ Award,
+ Send,
+ X,
+} from "lucide-react";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
+
+interface CreatorMatch {
+ id: string;
+ display_name: string;
+ tagline: string | null;
+ bio: string | null;
+ profile_picture_url: string | null;
+ primary_niche: string;
+ secondary_niches: string[];
+ total_followers: number;
+ engagement_rate: number | null;
+ top_platforms: string[] | null;
+ match_score: number;
+ match_reasoning: string;
+ full_details: any;
+}
+
+export default function FindCreatorsPage() {
+ const params = useParams<{ campaign_id?: string | string[] }>();
+ const router = useRouter();
+ const campaignIdValue = Array.isArray(params?.campaign_id)
+ ? params?.campaign_id[0]
+ : params?.campaign_id;
+ const campaignId = campaignIdValue ?? "";
+
+ const [campaign, setCampaign] = useState(null);
+ const [creators, setCreators] = useState([]);
+ const [manualCreators, setManualCreators] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [expandedCreator, setExpandedCreator] = useState(null);
+ const [showProposalModal, setShowProposalModal] = useState(null);
+ const [proposalData, setProposalData] = useState({
+ subject: "",
+ message: "",
+ proposed_amount: "",
+ content_idea: "",
+ ideal_pricing: "",
+ });
+ const [draftingProposal, setDraftingProposal] = useState(false);
+ const [sendingProposal, setSendingProposal] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [searchingCreator, setSearchingCreator] = useState(false);
+ const [searchError, setSearchError] = useState(null);
+ const [multipleMatches, setMultipleMatches] = useState(null);
+
+ useEffect(() => {
+ if (!campaignId) return;
+ loadData();
+ }, [campaignId]);
+
+ const loadData = async () => {
+ if (!campaignId) {
+ setError("Campaign ID is missing. Please return to campaigns and try again.");
+ return;
+ }
+ try {
+ setLoading(true);
+ setError(null);
+
+ // Load campaign
+ const campaignData = await fetchCampaignById(campaignId);
+ setCampaign(campaignData);
+
+ // Load matching creators
+ const url = `${API_BASE_URL}/campaigns/${campaignId}/find-creators?limit=4`;
+ const response = await authenticatedFetch(url);
+
+ if (!response.ok) {
+ throw new Error("Failed to find creators");
+ }
+
+ const creatorsData = await response.json();
+ setCreators(creatorsData);
+ } catch (err: any) {
+ setError(err.message || "Failed to load data");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDraftProposal = async (creatorId: string) => {
+ try {
+ setDraftingProposal(true);
+ if (!campaignId) {
+ alert("Campaign ID is missing. Please go back and try again.");
+ return;
+ }
+ let url = `${API_BASE_URL}/proposals/draft?campaign_id=${campaignId}&creator_id=${creatorId}`;
+ if (proposalData.content_idea) {
+ url += `&content_idea=${encodeURIComponent(proposalData.content_idea)}`;
+ }
+ if (proposalData.ideal_pricing) {
+ url += `&ideal_pricing=${encodeURIComponent(proposalData.ideal_pricing)}`;
+ }
+
+ const response = await authenticatedFetch(url);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || "Failed to draft proposal");
+ }
+
+ const draft = await response.json();
+ setProposalData({
+ ...proposalData,
+ subject: draft.subject || "",
+ message: draft.message || "",
+ });
+ } catch (err: any) {
+ alert("Failed to draft proposal: " + err.message);
+ } finally {
+ setDraftingProposal(false);
+ }
+ };
+
+ const handleSearchCreator = async () => {
+ if (!searchQuery.trim()) {
+ setSearchError("Please enter a creator ID or name");
+ return;
+ }
+
+ try {
+ if (!campaignId) {
+ setSearchError("Campaign ID missing. Please go back and try again.");
+ return;
+ }
+ setSearchingCreator(true);
+ setSearchError(null);
+ setMultipleMatches(null);
+
+ const url = `${API_BASE_URL}/campaigns/${campaignId}/search-creator?query=${encodeURIComponent(searchQuery.trim())}`;
+ const response = await authenticatedFetch(url);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || "Failed to search creator");
+ }
+
+ const result = await response.json();
+
+ if (result.multiple_matches && result.creators) {
+ // Show multiple matches for user to choose
+ setMultipleMatches(result.creators);
+ } else {
+ // Single creator found
+ const creatorMatch: CreatorMatch = {
+ id: result.id,
+ display_name: result.display_name,
+ tagline: result.tagline,
+ bio: result.bio,
+ profile_picture_url: result.profile_picture_url,
+ primary_niche: result.primary_niche,
+ secondary_niches: result.secondary_niches,
+ total_followers: result.total_followers,
+ engagement_rate: result.engagement_rate,
+ top_platforms: result.top_platforms,
+ match_score: 0, // Manual search doesn't have a match score
+ match_reasoning: "Manually searched creator",
+ full_details: result.full_details,
+ };
+
+ // Add to manual creators list if not already there
+ setManualCreators((prev) => {
+ if (prev.some((c) => c.id === creatorMatch.id)) {
+ return prev;
+ }
+ return [...prev, creatorMatch];
+ });
+
+ setSearchQuery("");
+ }
+ } catch (err: any) {
+ setSearchError(err.message || "Failed to search creator");
+ } finally {
+ setSearchingCreator(false);
+ }
+ };
+
+ const handleSelectFromMatches = (creator: any) => {
+ const creatorMatch: CreatorMatch = {
+ id: creator.id,
+ display_name: creator.display_name,
+ tagline: creator.tagline,
+ bio: null,
+ profile_picture_url: creator.profile_picture_url,
+ primary_niche: creator.primary_niche,
+ secondary_niches: [],
+ total_followers: creator.total_followers,
+ engagement_rate: creator.engagement_rate,
+ top_platforms: null,
+ match_score: 0,
+ match_reasoning: "Manually searched creator",
+ full_details: creator,
+ };
+
+ setManualCreators((prev) => {
+ if (prev.some((c) => c.id === creatorMatch.id)) {
+ return prev;
+ }
+ return [...prev, creatorMatch];
+ });
+
+ setMultipleMatches(null);
+ setSearchQuery("");
+ };
+
+ const handleSendProposal = async (creatorId: string) => {
+ try {
+ if (!campaignId) {
+ alert("Campaign ID is missing. Please go back and try again.");
+ return;
+ }
+ setSendingProposal(true);
+ const url = `${API_BASE_URL}/proposals`;
+ const response = await authenticatedFetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ campaign_id: campaignId,
+ creator_id: creatorId,
+ subject: proposalData.subject,
+ message: proposalData.message,
+ proposed_amount: proposalData.proposed_amount
+ ? parseFloat(proposalData.proposed_amount)
+ : null,
+ content_ideas: proposalData.content_idea
+ ? [proposalData.content_idea]
+ : [],
+ ideal_pricing: proposalData.ideal_pricing || null,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || "Failed to send proposal");
+ }
+
+ alert("Proposal sent successfully!");
+ setShowProposalModal(null);
+ setProposalData({
+ subject: "",
+ message: "",
+ proposed_amount: "",
+ content_idea: "",
+ ideal_pricing: "",
+ });
+ } catch (err: any) {
+ alert("Failed to send proposal: " + err.message);
+ } finally {
+ setSendingProposal(false);
+ }
+ };
+
+ if (!campaignId) {
+ return (
+
+
+
+
Invalid campaign
+
+ We couldn't determine which campaign you're trying to manage. Please return to the campaigns list and try again.
+
+
router.push("/brand/campaigns")}
+ className="mt-6 rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-blue-700"
+ >
+ Go to campaigns
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {/* Header */}
+
+
router.push("/brand/campaigns")}
+ className="mb-4 text-blue-600 hover:text-blue-800"
+ >
+ ← Back to Campaigns
+
+
Find Creators
+ {campaign && (
+
+ Matching creators for: {campaign.title}
+
+ )}
+
+
+ {/* Loading State */}
+ {loading && (
+
+
+
Finding matching creators...
+
+ )}
+
+ {/* Error State */}
+ {error && (
+
+
{error}
+
+ Try again
+
+
+ )}
+
+ {/* Manual Creator Search Section */}
+ {!loading && !error && (
+
+
+ Search Creator by ID or Name
+
+
+
+
+
+ {
+ setSearchQuery(e.target.value);
+ setSearchError(null);
+ }}
+ onKeyPress={(e) => e.key === "Enter" && handleSearchCreator()}
+ placeholder="Enter creator ID (UUID) or display name"
+ className="w-full rounded-lg border border-gray-300 py-3 pr-4 pl-10 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ />
+
+ {searchError && (
+
{searchError}
+ )}
+
+
+ {searchingCreator ? (
+ <>
+
+ Searching...
+ >
+ ) : (
+ "Search"
+ )}
+
+
+
+ {/* Multiple Matches Modal */}
+ {multipleMatches && multipleMatches.length > 0 && (
+
+
+ Multiple creators found. Select one:
+
+
+ {multipleMatches.map((creator) => (
+
handleSelectFromMatches(creator)}
+ className="w-full rounded-lg border border-gray-300 bg-white p-3 text-left transition-colors hover:bg-gray-50"
+ >
+
+ {creator.profile_picture_url && (
+
+ )}
+
+
+ {creator.display_name}
+
+ {creator.tagline && (
+
+ {creator.tagline}
+
+ )}
+
+ {creator.total_followers.toLocaleString()} followers
+ {creator.engagement_rate &&
+ ` • ${creator.engagement_rate.toFixed(1)}% engagement`}
+
+
+
+
+ ))}
+
+
{
+ setMultipleMatches(null);
+ setSearchQuery("");
+ }}
+ className="mt-3 text-sm text-gray-600 hover:text-gray-800"
+ >
+ Cancel
+
+
+ )}
+
+ )}
+
+ {/* AI Matched Creators Section */}
+ {!loading && !error && creators.length > 0 && (
+
+
+ AI Matched Creators
+
+
+ {creators.map((creator) => (
+
+ {/* Creator Summary */}
+
+
+
+ {creator.profile_picture_url ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+ {creator.display_name}
+
+ {creator.tagline && (
+
{creator.tagline}
+ )}
+
+
+ {creator.primary_niche}
+
+ {creator.top_platforms &&
+ creator.top_platforms.map((platform) => (
+
+ {platform}
+
+ ))}
+
+
+
+
+
+
+ {creator.match_score}% Match
+
+
+
+ {creator.total_followers.toLocaleString()} followers
+
+ {creator.engagement_rate && (
+
+ {creator.engagement_rate.toFixed(1)}% engagement
+
+ )}
+
+
+
+ {creator.match_reasoning}
+
+
+
+ setExpandedCreator(
+ expandedCreator === creator.id ? null : creator.id
+ )
+ }
+ className="flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 transition-colors hover:bg-gray-50"
+ >
+ {expandedCreator === creator.id ? (
+ <>
+
+ Hide Details
+ >
+ ) : (
+ <>
+
+ View Details
+ >
+ )}
+
+ {
+ setShowProposalModal(creator.id);
+ setProposalData({
+ subject: "",
+ message: "",
+ proposed_amount: "",
+ content_idea: "",
+ ideal_pricing: "",
+ });
+ }}
+ className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-purple-500 to-purple-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:from-purple-600 hover:to-purple-700"
+ >
+
+ Send Proposal
+
+
+
+
+
+
+ {/* Expanded Details */}
+ {expandedCreator === creator.id && creator.full_details && (
+
+
+
+
Bio
+
+ {creator.full_details.bio || "No bio available"}
+
+
+
+
+ Why They Fit
+
+
{creator.match_reasoning}
+
+ {creator.full_details.secondary_niches &&
+ creator.full_details.secondary_niches.length > 0 && (
+
+
+ Secondary Niches
+
+
+ {creator.full_details.secondary_niches.map(
+ (niche: string) => (
+
+ {niche}
+
+ )
+ )}
+
+
+ )}
+ {creator.full_details.years_of_experience && (
+
+
+ Experience
+
+
+ {creator.full_details.years_of_experience} years
+
+
+ )}
+
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* Manually Searched Creators Section */}
+ {manualCreators.length > 0 && (
+
+
+ Manually Searched Creators
+
+
+ {manualCreators.map((creator) => (
+
+ {/* Creator Summary */}
+
+
+
+ {creator.profile_picture_url ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+ {creator.display_name}
+
+ {creator.tagline && (
+
{creator.tagline}
+ )}
+
+
+ {creator.primary_niche}
+
+ {creator.top_platforms &&
+ creator.top_platforms.map((platform) => (
+
+ {platform}
+
+ ))}
+
+
+
+
+ {creator.total_followers.toLocaleString()} followers
+
+ {creator.engagement_rate && (
+
+ {creator.engagement_rate.toFixed(1)}% engagement
+
+ )}
+
+
+
+
+ setExpandedCreator(
+ expandedCreator === creator.id ? null : creator.id
+ )
+ }
+ className="flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 transition-colors hover:bg-gray-50"
+ >
+ {expandedCreator === creator.id ? (
+ <>
+
+ Hide Details
+ >
+ ) : (
+ <>
+
+ View Details
+ >
+ )}
+
+ {
+ setShowProposalModal(creator.id);
+ setProposalData({
+ subject: "",
+ message: "",
+ proposed_amount: "",
+ content_idea: "",
+ ideal_pricing: "",
+ });
+ }}
+ className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-purple-500 to-purple-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:from-purple-600 hover:to-purple-700"
+ >
+
+ Send Proposal
+
+ {
+ setManualCreators((prev) =>
+ prev.filter((c) => c.id !== creator.id)
+ );
+ }}
+ className="flex items-center gap-2 rounded-lg border border-red-300 bg-white px-4 py-2 text-sm font-semibold text-red-600 transition-colors hover:bg-red-50"
+ >
+
+ Remove
+
+
+
+
+
+
+ {/* Expanded Details */}
+ {expandedCreator === creator.id && creator.full_details && (
+
+
+
+
Bio
+
+ {creator.full_details.bio || "No bio available"}
+
+
+ {creator.full_details.secondary_niches &&
+ creator.full_details.secondary_niches.length > 0 && (
+
+
+ Secondary Niches
+
+
+ {creator.full_details.secondary_niches.map(
+ (niche: string) => (
+
+ {niche}
+
+ )
+ )}
+
+
+ )}
+ {creator.full_details.years_of_experience && (
+
+
+ Experience
+
+
+ {creator.full_details.years_of_experience} years
+
+
+ )}
+
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* Empty State */}
+ {!loading && !error && creators.length === 0 && manualCreators.length === 0 && (
+
+
+
+ No matching creators found
+
+
+ Try adjusting your campaign requirements or check back later.
+
+
+ )}
+
+
+ {/* Proposal Modal */}
+ {showProposalModal && (
+
+
+
+
Send Proposal
+ setShowProposalModal(null)}
+ className="text-gray-400 hover:text-gray-600"
+ >
+
+
+
+
+
+
+
+ Content Idea (optional)
+
+
+ setProposalData({ ...proposalData, content_idea: e.target.value })
+ }
+ className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2"
+ placeholder="Brief content idea or concept"
+ />
+
+
+
+
+ Ideal Pricing (optional)
+
+
+ setProposalData({
+ ...proposalData,
+ ideal_pricing: e.target.value,
+ })
+ }
+ className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2"
+ placeholder="e.g., 50,000 - 75,000 INR"
+ />
+
+
+
+ handleDraftProposal(showProposalModal)}
+ disabled={draftingProposal}
+ className="flex-1 rounded-lg border border-gray-300 bg-white px-4 py-2 font-semibold text-gray-700 transition-colors hover:bg-gray-50 disabled:opacity-50"
+ >
+ {draftingProposal ? (
+ <>
+
+ Drafting...
+ >
+ ) : (
+ "AI Draft Proposal"
+ )}
+
+
+
+
+
+ Subject *
+
+
+ setProposalData({ ...proposalData, subject: e.target.value })
+ }
+ className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2"
+ required
+ />
+
+
+
+
+ Message *
+
+
+ setProposalData({ ...proposalData, message: e.target.value })
+ }
+ rows={8}
+ className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2"
+ required
+ />
+
+
+
+
+ Proposed Amount (INR, optional)
+
+
+ setProposalData({
+ ...proposalData,
+ proposed_amount: e.target.value,
+ })
+ }
+ className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2"
+ placeholder="50000"
+ />
+
+
+
+ setShowProposalModal(null)}
+ className="flex-1 rounded-lg border border-gray-300 bg-white px-4 py-2 font-semibold text-gray-700 transition-colors hover:bg-gray-50"
+ >
+ Cancel
+
+ handleSendProposal(showProposalModal)}
+ disabled={
+ sendingProposal ||
+ !proposalData.subject ||
+ !proposalData.message
+ }
+ className="flex-1 rounded-lg bg-gradient-to-r from-purple-500 to-purple-600 px-4 py-2 font-semibold text-white transition-colors hover:from-purple-600 hover:to-purple-700 disabled:opacity-50"
+ >
+ {sendingProposal ? (
+ <>
+
+ Sending...
+ >
+ ) : (
+ "Send Proposal"
+ )}
+
+
+
+
+
+ )}
+
+
+ );
+}
+
diff --git a/frontend/app/brand/campaigns/create/page.tsx b/frontend/app/brand/campaigns/create/page.tsx
new file mode 100644
index 0000000..2f22f97
--- /dev/null
+++ b/frontend/app/brand/campaigns/create/page.tsx
@@ -0,0 +1,713 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import { getBrandDashboardStats } from "@/lib/api/analytics";
+import { createCampaign } from "@/lib/campaignApi";
+import {
+ AGE_GROUP_OPTIONS,
+ CampaignDeliverable,
+ CampaignFormData,
+ CONTENT_TYPE_OPTIONS,
+ FOLLOWER_RANGE_OPTIONS,
+ GENDER_OPTIONS,
+ INCOME_LEVEL_OPTIONS,
+ NICHE_OPTIONS,
+ PLATFORM_OPTIONS,
+} from "@/types/campaign";
+import { ArrowLeft, Eye, Plus, Save, Trash2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function CreateCampaignPage() {
+ const router = useRouter();
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const [formData, setFormData] = useState({
+ title: "",
+ short_description: "",
+ description: "",
+ status: "draft",
+ platforms: [],
+ deliverables: [],
+ target_audience: {},
+ budget_min: "",
+ budget_max: "",
+ preferred_creator_niches: [],
+ preferred_creator_followers_range: "",
+ starts_at: "",
+ ends_at: "",
+ });
+
+ const [newDeliverable, setNewDeliverable] = useState({
+ platform: "",
+ content_type: "",
+ quantity: 1,
+ guidance: "",
+ required: true,
+ });
+
+ // Stats state
+ const [stats, setStats] = useState({
+ totalCampaigns: 0,
+ activeCampaigns: 0,
+ applications: 0,
+ });
+ const [statsLoading, setStatsLoading] = useState(true);
+
+ const updateField = (field: keyof CampaignFormData, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const toggleArrayField = (
+ field: "platforms" | "preferred_creator_niches",
+ value: string
+ ) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: prev[field].includes(value)
+ ? prev[field].filter((item) => item !== value)
+ : [...prev[field], value],
+ }));
+ };
+
+ const updateTargetAudience = (field: string, value: any) => {
+ setFormData((prev) => ({
+ ...prev,
+ target_audience: {
+ ...prev.target_audience,
+ [field]: value,
+ },
+ }));
+ };
+
+ const toggleTargetAudienceArray = (field: string, value: string) => {
+ const current =
+ (formData.target_audience[
+ field as keyof typeof formData.target_audience
+ ] as string[]) || [];
+ updateTargetAudience(
+ field,
+ current.includes(value)
+ ? current.filter((item) => item !== value)
+ : [...current, value]
+ );
+ };
+
+ const addDeliverable = () => {
+ if (!newDeliverable.platform || !newDeliverable.content_type) {
+ setError(
+ "Please select both platform and content type for the deliverable"
+ );
+ return;
+ }
+ setError(null); // Clear any previous errors
+ setFormData((prev) => ({
+ ...prev,
+ deliverables: [...prev.deliverables, { ...newDeliverable }],
+ }));
+ setNewDeliverable({
+ platform: "",
+ content_type: "",
+ quantity: 1,
+ guidance: "",
+ required: true,
+ });
+ };
+
+ const removeDeliverable = (index: number) => {
+ setFormData((prev) => ({
+ ...prev,
+ deliverables: prev.deliverables.filter((_, i) => i !== index),
+ }));
+ };
+
+ const validateForm = (): boolean => {
+ if (!formData.title.trim()) {
+ setError("Campaign title is required");
+ return false;
+ }
+ if (formData.budget_min && formData.budget_max) {
+ if (parseFloat(formData.budget_min) > parseFloat(formData.budget_max)) {
+ setError("Minimum budget cannot be greater than maximum budget");
+ return false;
+ }
+ }
+ if (formData.starts_at && formData.ends_at) {
+ if (new Date(formData.starts_at) > new Date(formData.ends_at)) {
+ setError("Start date cannot be after end date");
+ return false;
+ }
+ }
+ return true;
+ };
+
+ const handleSubmit = async (status: "draft" | "active") => {
+ setError(null);
+
+ if (!validateForm()) {
+ return;
+ }
+
+ try {
+ setLoading(true);
+ const submitData = {
+ ...formData,
+ status,
+ budget_min: formData.budget_min
+ ? parseFloat(formData.budget_min)
+ : undefined,
+ budget_max: formData.budget_max
+ ? parseFloat(formData.budget_max)
+ : undefined,
+ starts_at: formData.starts_at || undefined,
+ ends_at: formData.ends_at || undefined,
+ };
+ await createCampaign(submitData);
+ // Refresh stats before navigating
+ await loadStats();
+ router.push("/brand/campaigns");
+ } catch (err: any) {
+ setError(err.message || "Failed to create campaign");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Load stats on mount
+ useEffect(() => {
+ loadStats();
+ }, []);
+
+ const loadStats = async () => {
+ try {
+ setStatsLoading(true);
+ const dashboardStats = await getBrandDashboardStats();
+ setStats({
+ totalCampaigns: dashboardStats.overview.total_campaigns,
+ activeCampaigns: dashboardStats.overview.active_campaigns,
+ applications: dashboardStats.overview.total_proposals,
+ });
+ } catch (err) {
+ console.error("Failed to load stats:", err);
+ } finally {
+ setStatsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {/* Header */}
+
+
router.back()}
+ className="mb-4 flex items-center gap-2 text-gray-600 transition-colors hover:text-gray-900"
+ >
+
+ Back
+
+
+ Create New Campaign
+
+
+ Fill out the details below to launch your influencer marketing
+ campaign
+
+
+
+ {/* Error Message */}
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {/* Basic Information */}
+
+
+ Basic Information
+
+
+
+
+ Campaign Title *
+
+ updateField("title", e.target.value)}
+ placeholder="e.g., Summer Product Launch 2024"
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ required
+ />
+
+
+
+
+ Short Description
+
+
+ updateField("short_description", e.target.value)
+ }
+ placeholder="Brief one-liner about your campaign"
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ />
+
+
+
+
+ Detailed Description
+
+ updateField("description", e.target.value)}
+ placeholder="Provide detailed information about your campaign goals, requirements, and expectations..."
+ rows={6}
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ />
+
+
+
+
+ {/* Campaign Duration */}
+
+
+ Campaign Duration
+
+
+
+
+ {/* Budget */}
+
+
+ Budget Range (INR)
+
+
+
+
+ {/* Platforms */}
+
+
+ Target Platforms
+
+
+ {PLATFORM_OPTIONS.map((platform) => (
+ toggleArrayField("platforms", platform)}
+ className={`rounded-lg border-2 px-4 py-3 font-medium transition-all ${
+ formData.platforms.includes(platform)
+ ? "border-blue-500 bg-blue-50 text-blue-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-blue-300"
+ }`}
+ >
+ {platform}
+
+ ))}
+
+
+
+ {/* Deliverables */}
+
+
+ Campaign Deliverables
+
+
+ {/* Add Deliverable Form */}
+
+
+ Add Deliverable
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ platform: e.target.value,
+ })
+ }
+ className="rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none"
+ >
+ Select Platform
+ {PLATFORM_OPTIONS.map((platform) => (
+
+ {platform}
+
+ ))}
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ content_type: e.target.value,
+ })
+ }
+ className="rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none"
+ >
+ Select Content Type
+ {CONTENT_TYPE_OPTIONS.map((type) => (
+
+ {type}
+
+ ))}
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ quantity: parseInt(e.target.value) || 1,
+ })
+ }
+ min="1"
+ placeholder="Quantity"
+ className="rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none"
+ />
+
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ guidance: e.target.value,
+ })
+ }
+ placeholder="Additional guidance or requirements (optional)"
+ rows={2}
+ className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none"
+ />
+
+
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ required: e.target.checked,
+ })
+ }
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ />
+
+ Required deliverable
+
+
+
+
+
+ Add
+
+
+
+
+ {/* Deliverables List */}
+ {formData.deliverables.length > 0 && (
+
+
+ Added Deliverables ({formData.deliverables.length})
+
+ {formData.deliverables.map((deliverable, index) => (
+
+
+
+
+ {deliverable.platform} - {deliverable.content_type}
+
+
+ (Qty: {deliverable.quantity})
+
+ {deliverable.required && (
+
+ Required
+
+ )}
+
+ {deliverable.guidance && (
+
+ {deliverable.guidance}
+
+ )}
+
+
removeDeliverable(index)}
+ className="ml-2 text-red-500 transition-colors hover:text-red-700"
+ >
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Creator Preferences */}
+
+
+ Creator Preferences
+
+
+
+
+
+ Preferred Niches
+
+
+ {NICHE_OPTIONS.map((niche) => (
+
+ toggleArrayField("preferred_creator_niches", niche)
+ }
+ className={`rounded-lg border-2 px-3 py-2 text-sm font-medium transition-all ${
+ formData.preferred_creator_niches.includes(niche)
+ ? "border-purple-500 bg-purple-50 text-purple-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-purple-300"
+ }`}
+ >
+ {niche}
+
+ ))}
+
+
+
+
+
+ Preferred Follower Range
+
+
+ updateField(
+ "preferred_creator_followers_range",
+ e.target.value
+ )
+ }
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ >
+ Select a range
+ {FOLLOWER_RANGE_OPTIONS.map((range) => (
+
+ {range}
+
+ ))}
+
+
+
+
+
+ {/* Target Audience */}
+
+
+ Target Audience
+
+
+
+
+
+ Age Groups
+
+
+ {AGE_GROUP_OPTIONS.map((age) => (
+
+ toggleTargetAudienceArray("age_groups", age)
+ }
+ className={`rounded-lg border-2 px-4 py-2 text-sm font-medium transition-all ${
+ (formData.target_audience.age_groups || []).includes(
+ age
+ )
+ ? "border-blue-500 bg-blue-50 text-blue-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-blue-300"
+ }`}
+ >
+ {age}
+
+ ))}
+
+
+
+
+
+ Gender
+
+
+ {GENDER_OPTIONS.map((gender) => (
+
+ toggleTargetAudienceArray("gender", gender)
+ }
+ className={`rounded-lg border-2 px-4 py-2 text-sm font-medium transition-all ${
+ (formData.target_audience.gender || []).includes(
+ gender
+ )
+ ? "border-blue-500 bg-blue-50 text-blue-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-blue-300"
+ }`}
+ >
+ {gender}
+
+ ))}
+
+
+
+
+
+ Income Levels
+
+
+ {INCOME_LEVEL_OPTIONS.map((income) => (
+
+ toggleTargetAudienceArray("income_level", income)
+ }
+ className={`rounded-lg border-2 px-3 py-2 text-sm font-medium transition-all ${
+ (
+ formData.target_audience.income_level || []
+ ).includes(income)
+ ? "border-blue-500 bg-blue-50 text-blue-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-blue-300"
+ }`}
+ >
+ {income}
+
+ ))}
+
+
+
+
+
+ Audience Description
+
+
+ updateTargetAudience("description", e.target.value)
+ }
+ placeholder="Describe your target audience in detail..."
+ rows={3}
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ />
+
+
+
+
+ {/* Action Buttons */}
+
+ router.back()}
+ className="rounded-lg border border-gray-300 bg-white px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-50"
+ disabled={loading}
+ >
+ Cancel
+
+ handleSubmit("draft")}
+ disabled={loading}
+ className="flex items-center justify-center gap-2 rounded-lg bg-gray-600 px-6 py-3 font-semibold text-white transition-colors hover:bg-gray-700 disabled:opacity-50"
+ >
+
+ Save as Draft
+
+ handleSubmit("active")}
+ disabled={loading}
+ className="flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-purple-500 to-purple-600 px-6 py-3 font-semibold text-white transition-colors hover:from-purple-600 hover:to-purple-700 disabled:opacity-50"
+ >
+
+ {loading ? "Publishing..." : "Publish Campaign"}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/brand/campaigns/edit/[id]/page.tsx b/frontend/app/brand/campaigns/edit/[id]/page.tsx
new file mode 100644
index 0000000..3ca1867
--- /dev/null
+++ b/frontend/app/brand/campaigns/edit/[id]/page.tsx
@@ -0,0 +1,807 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import { fetchCampaignById, updateCampaign } from "@/lib/campaignApi";
+import {
+ AGE_GROUP_OPTIONS,
+ CampaignDeliverable,
+ CampaignFormData,
+ CampaignPayload,
+ CONTENT_TYPE_OPTIONS,
+ FOLLOWER_RANGE_OPTIONS,
+ GENDER_OPTIONS,
+ INCOME_LEVEL_OPTIONS,
+ NICHE_OPTIONS,
+ PLATFORM_OPTIONS,
+} from "@/types/campaign";
+import { ArrowLeft, Eye, Plus, Save, Trash2 } from "lucide-react";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function EditCampaignPage() {
+ const router = useRouter();
+ const params = useParams<{ id?: string | string[] }>();
+ const campaignIdValue = Array.isArray(params?.id) ? params?.id[0] : params?.id;
+ const campaignId = campaignIdValue ?? "";
+
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+
+ const [formData, setFormData] = useState({
+ title: "",
+ short_description: "",
+ description: "",
+ status: "draft",
+ platforms: [],
+ deliverables: [],
+ target_audience: {},
+ budget_min: "",
+ budget_max: "",
+ preferred_creator_niches: [],
+ preferred_creator_followers_range: "",
+ starts_at: "",
+ ends_at: "",
+ });
+
+ const [newDeliverable, setNewDeliverable] = useState({
+ platform: "",
+ content_type: "",
+ quantity: 1,
+ guidance: "",
+ required: true,
+ });
+
+ // Load campaign data
+ useEffect(() => {
+ const loadCampaign = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const campaign = await fetchCampaignById(campaignId);
+
+ // Convert campaign data to form data format
+ setFormData({
+ title: campaign.title || "",
+ short_description: campaign.short_description || "",
+ description: campaign.description || "",
+ status: campaign.status,
+ platforms: campaign.platforms || [],
+ deliverables: campaign.deliverables || [],
+ target_audience: campaign.target_audience || {},
+ budget_min: campaign.budget_min?.toString() || "",
+ budget_max: campaign.budget_max?.toString() || "",
+ preferred_creator_niches: campaign.preferred_creator_niches || [],
+ preferred_creator_followers_range:
+ campaign.preferred_creator_followers_range || "",
+ starts_at: campaign.starts_at
+ ? new Date(campaign.starts_at).toISOString().split("T")[0]
+ : "",
+ ends_at: campaign.ends_at
+ ? new Date(campaign.ends_at).toISOString().split("T")[0]
+ : "",
+ });
+ } catch (err: any) {
+ setError(err.message || "Failed to load campaign");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (campaignId) {
+ loadCampaign();
+ }
+ }, [campaignId]);
+
+ const updateField = (field: keyof CampaignFormData, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const toggleArrayField = (
+ field: "platforms" | "preferred_creator_niches",
+ value: string
+ ) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: prev[field].includes(value)
+ ? prev[field].filter((item) => item !== value)
+ : [...prev[field], value],
+ }));
+ };
+
+ const updateTargetAudience = (field: string, value: any) => {
+ setFormData((prev) => ({
+ ...prev,
+ target_audience: {
+ ...prev.target_audience,
+ [field]: value,
+ },
+ }));
+ };
+
+ const toggleTargetAudienceArray = (field: string, value: string) => {
+ const current =
+ (formData.target_audience[
+ field as keyof typeof formData.target_audience
+ ] as string[]) || [];
+ updateTargetAudience(
+ field,
+ current.includes(value)
+ ? current.filter((item) => item !== value)
+ : [...current, value]
+ );
+ };
+
+ const addDeliverable = () => {
+ if (!newDeliverable.platform || !newDeliverable.content_type) {
+ setError(
+ "Please select both platform and content type for the deliverable"
+ );
+ return;
+ }
+ setError(null);
+ setFormData((prev) => ({
+ ...prev,
+ deliverables: [...prev.deliverables, { ...newDeliverable }],
+ }));
+ setNewDeliverable({
+ platform: "",
+ content_type: "",
+ quantity: 1,
+ guidance: "",
+ required: true,
+ });
+ };
+
+ const removeDeliverable = (index: number) => {
+ setFormData((prev) => ({
+ ...prev,
+ deliverables: prev.deliverables.filter((_, i) => i !== index),
+ }));
+ };
+
+ const validateForm = (): boolean => {
+ if (!formData.title.trim()) {
+ setError("Campaign title is required");
+ return false;
+ }
+ if (formData.budget_min && formData.budget_max) {
+ if (parseFloat(formData.budget_min) > parseFloat(formData.budget_max)) {
+ setError("Minimum budget cannot be greater than maximum budget");
+ return false;
+ }
+ }
+ if (formData.starts_at && formData.ends_at) {
+ if (new Date(formData.starts_at) > new Date(formData.ends_at)) {
+ setError("Start date cannot be after end date");
+ return false;
+ }
+ }
+ return true;
+ };
+
+ const handleSubmit = async (status?: "draft" | "active") => {
+ setError(null);
+
+ if (!campaignId) {
+ setError("Campaign ID is missing. Please navigate back and try again.");
+ return;
+ }
+
+ if (!validateForm()) {
+ return;
+ }
+
+ try {
+ setSaving(true);
+ const submitData: Partial = {
+ ...formData,
+ budget_min: formData.budget_min
+ ? parseFloat(formData.budget_min)
+ : undefined,
+ budget_max: formData.budget_max
+ ? parseFloat(formData.budget_max)
+ : undefined,
+ starts_at: formData.starts_at || undefined,
+ ends_at: formData.ends_at || undefined,
+ };
+
+ if (status) {
+ submitData.status = status;
+ }
+
+ await updateCampaign(campaignId, submitData);
+ router.push("/brand/campaigns");
+ } catch (err: any) {
+ setError(err.message || "Failed to update campaign");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (!campaignId) {
+ return (
+
+
+
+
+
+ Invalid campaign
+
+
+ We couldn't determine which campaign you wanted to edit. Please
+ return to your campaigns list and try again.
+
+ router.push("/brand/campaigns")}
+ className="mt-6 rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-blue-700"
+ >
+ Go to campaigns
+
+
+
+
+ );
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+
Loading campaign...
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {/* Header */}
+
+
router.back()}
+ className="mb-4 flex items-center gap-2 text-gray-600 transition-colors hover:text-gray-900"
+ >
+
+ Back
+
+
+ Edit Campaign
+
+
+ Update your campaign details below
+
+
+
+ {/* Error Message */}
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {/* Basic Information */}
+
+
+ Basic Information
+
+
+
+
+ Campaign Title *
+
+ updateField("title", e.target.value)}
+ placeholder="e.g., Summer Product Launch 2024"
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ required
+ />
+
+
+
+
+ Short Description
+
+
+ updateField("short_description", e.target.value)
+ }
+ placeholder="Brief one-liner about your campaign"
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ />
+
+
+
+
+ Detailed Description
+
+ updateField("description", e.target.value)}
+ placeholder="Provide detailed information about your campaign goals, requirements, and expectations..."
+ rows={6}
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ />
+
+
+
+
+ {/* Campaign Duration */}
+
+
+ Campaign Duration
+
+
+
+
+ {/* Budget */}
+
+
+ Budget Range (INR)
+
+
+
+
+ {/* Platforms */}
+
+
+ Target Platforms
+
+
+ {PLATFORM_OPTIONS.map((platform) => (
+ toggleArrayField("platforms", platform)}
+ className={`rounded-lg border-2 px-4 py-3 font-medium transition-all ${
+ formData.platforms.includes(platform)
+ ? "border-blue-500 bg-blue-50 text-blue-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-blue-300"
+ }`}
+ >
+ {platform}
+
+ ))}
+
+
+
+ {/* Deliverables */}
+
+
+ Campaign Deliverables
+
+
+ {/* Add Deliverable Form */}
+
+
+ Add Deliverable
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ platform: e.target.value,
+ })
+ }
+ className="rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none"
+ >
+ Select Platform
+ {PLATFORM_OPTIONS.map((platform) => (
+
+ {platform}
+
+ ))}
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ content_type: e.target.value,
+ })
+ }
+ className="rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none"
+ >
+ Select Content Type
+ {CONTENT_TYPE_OPTIONS.map((type) => (
+
+ {type}
+
+ ))}
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ quantity: parseInt(e.target.value) || 1,
+ })
+ }
+ min="1"
+ placeholder="Quantity"
+ className="rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none"
+ />
+
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ guidance: e.target.value,
+ })
+ }
+ placeholder="Additional guidance or requirements (optional)"
+ rows={2}
+ className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none"
+ />
+
+
+
+
+
+ setNewDeliverable({
+ ...newDeliverable,
+ required: e.target.checked,
+ })
+ }
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ />
+
+ Required deliverable
+
+
+
+
+
+ Add
+
+
+
+
+ {/* Deliverables List */}
+ {formData.deliverables.length > 0 && (
+
+
+ Added Deliverables ({formData.deliverables.length})
+
+ {formData.deliverables.map((deliverable, index) => (
+
+
+
+
+ {deliverable.platform} - {deliverable.content_type}
+
+
+ (Qty: {deliverable.quantity})
+
+ {deliverable.required && (
+
+ Required
+
+ )}
+
+ {deliverable.guidance && (
+
+ {deliverable.guidance}
+
+ )}
+
+
removeDeliverable(index)}
+ className="ml-2 text-red-500 transition-colors hover:text-red-700"
+ >
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Creator Preferences */}
+
+
+ Creator Preferences
+
+
+
+
+
+ Preferred Niches
+
+
+ {NICHE_OPTIONS.map((niche) => (
+
+ toggleArrayField("preferred_creator_niches", niche)
+ }
+ className={`rounded-lg border-2 px-3 py-2 text-sm font-medium transition-all ${
+ formData.preferred_creator_niches.includes(niche)
+ ? "border-purple-500 bg-purple-50 text-purple-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-purple-300"
+ }`}
+ >
+ {niche}
+
+ ))}
+
+
+
+
+
+ Preferred Follower Range
+
+
+ updateField(
+ "preferred_creator_followers_range",
+ e.target.value
+ )
+ }
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ >
+ Select a range
+ {FOLLOWER_RANGE_OPTIONS.map((range) => (
+
+ {range}
+
+ ))}
+
+
+
+
+
+ {/* Target Audience */}
+
+
+ Target Audience
+
+
+
+
+
+ Age Groups
+
+
+ {AGE_GROUP_OPTIONS.map((age) => (
+
+ toggleTargetAudienceArray("age_groups", age)
+ }
+ className={`rounded-lg border-2 px-4 py-2 text-sm font-medium transition-all ${
+ (formData.target_audience.age_groups || []).includes(
+ age
+ )
+ ? "border-blue-500 bg-blue-50 text-blue-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-blue-300"
+ }`}
+ >
+ {age}
+
+ ))}
+
+
+
+
+
+ Gender
+
+
+ {GENDER_OPTIONS.map((gender) => (
+
+ toggleTargetAudienceArray("gender", gender)
+ }
+ className={`rounded-lg border-2 px-4 py-2 text-sm font-medium transition-all ${
+ (formData.target_audience.gender || []).includes(
+ gender
+ )
+ ? "border-blue-500 bg-blue-50 text-blue-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-blue-300"
+ }`}
+ >
+ {gender}
+
+ ))}
+
+
+
+
+
+ Income Levels
+
+
+ {INCOME_LEVEL_OPTIONS.map((income) => (
+
+ toggleTargetAudienceArray("income_level", income)
+ }
+ className={`rounded-lg border-2 px-3 py-2 text-sm font-medium transition-all ${
+ (
+ formData.target_audience.income_level || []
+ ).includes(income)
+ ? "border-blue-500 bg-blue-50 text-blue-700"
+ : "border-gray-300 bg-white text-gray-700 hover:border-blue-300"
+ }`}
+ >
+ {income}
+
+ ))}
+
+
+
+
+
+ Audience Description
+
+
+ updateTargetAudience("description", e.target.value)
+ }
+ placeholder="Describe your target audience in detail..."
+ rows={3}
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ />
+
+
+
+
+ {/* Status */}
+
+
+ Campaign Status
+
+
+
+ Current Status
+
+
+ updateField("status", e.target.value as any)
+ }
+ className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ >
+ Draft
+ Active
+ Paused
+ Completed
+ Archived
+
+
+
+
+ {/* Action Buttons */}
+
+ router.back()}
+ className="rounded-lg border border-gray-300 bg-white px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-50"
+ disabled={saving}
+ >
+ Cancel
+
+ handleSubmit()}
+ disabled={saving}
+ className="flex items-center justify-center gap-2 rounded-lg bg-gray-600 px-6 py-3 font-semibold text-white transition-colors hover:bg-gray-700 disabled:opacity-50"
+ >
+
+ {saving ? "Saving..." : "Save Changes"}
+
+ {formData.status !== "active" && (
+ handleSubmit("active")}
+ disabled={saving}
+ className="flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-purple-500 to-purple-600 px-6 py-3 font-semibold text-white transition-colors hover:from-purple-600 hover:to-purple-700 disabled:opacity-50"
+ >
+
+ {saving ? "Publishing..." : "Publish Campaign"}
+
+ )}
+
+
+
+
+
+ );
+}
+
diff --git a/frontend/app/brand/campaigns/page.tsx b/frontend/app/brand/campaigns/page.tsx
new file mode 100644
index 0000000..d2a5312
--- /dev/null
+++ b/frontend/app/brand/campaigns/page.tsx
@@ -0,0 +1,741 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import {
+ fetchCampaigns,
+ formatCurrency,
+ formatDate,
+ getStatusColor,
+} from "@/lib/campaignApi";
+import {
+ Campaign,
+ CampaignStatus,
+ PLATFORM_OPTIONS,
+ STATUS_OPTIONS,
+} from "@/types/campaign";
+import {
+ Calendar,
+ ChevronDown,
+ ChevronUp,
+ DollarSign,
+ Filter,
+ Plus,
+ Search,
+ Target,
+ Users,
+ Check,
+ X,
+ Clock,
+ UserCheck,
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { updateCampaign } from "@/lib/campaignApi";
+import {
+ fetchCampaignApplications,
+ updateApplicationStatus,
+ type CampaignApplication,
+} from "@/lib/api/campaignWall";
+
+export default function CampaignsPage() {
+ const router = useRouter();
+ const [campaigns, setCampaigns] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [statusFilter, setStatusFilter] = useState("");
+ const [platformFilter, setPlatformFilter] = useState("");
+ const [budgetMin, setBudgetMin] = useState("");
+ const [budgetMax, setBudgetMax] = useState("");
+ const [startsAfter, setStartsAfter] = useState("");
+ const [endsBefore, setEndsBefore] = useState("");
+ const [expandedCampaign, setExpandedCampaign] = useState(null);
+ const [openCampaignsFilter, setOpenCampaignsFilter] = useState(false);
+ const [applications, setApplications] = useState>({});
+ const [loadingApplications, setLoadingApplications] = useState>({});
+ const [updatingCampaign, setUpdatingCampaign] = useState(null);
+
+ useEffect(() => {
+ loadCampaigns();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ statusFilter,
+ platformFilter,
+ budgetMin,
+ budgetMax,
+ startsAfter,
+ endsBefore,
+ ]);
+
+ const loadCampaigns = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const filters = {
+ status: statusFilter || undefined,
+ search: searchTerm || undefined,
+ platform: platformFilter || undefined,
+ budget_min: budgetMin ? parseFloat(budgetMin) : undefined,
+ budget_max: budgetMax ? parseFloat(budgetMax) : undefined,
+ starts_after: startsAfter || undefined,
+ ends_before: endsBefore || undefined,
+ };
+ const data = await fetchCampaigns(filters);
+ setCampaigns(data);
+ } catch (err: any) {
+ setError(err.message || "Failed to load campaigns");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSearch = () => {
+ loadCampaigns();
+ };
+
+ const toggleExpand = async (campaignId: string) => {
+ if (expandedCampaign === campaignId) {
+ setExpandedCampaign(null);
+ } else {
+ setExpandedCampaign(campaignId);
+ // Load applications when expanding
+ await loadApplications(campaignId);
+ }
+ };
+
+ const loadApplications = async (campaignId: string) => {
+ if (loadingApplications[campaignId]) return;
+ try {
+ setLoadingApplications((prev) => ({ ...prev, [campaignId]: true }));
+ const apps = await fetchCampaignApplications(campaignId);
+ setApplications((prev) => ({ ...prev, [campaignId]: apps }));
+ } catch (err: any) {
+ console.error("Failed to load applications:", err);
+ setError(err.message || "Failed to load applications");
+ } finally {
+ setLoadingApplications((prev) => ({ ...prev, [campaignId]: false }));
+ }
+ };
+
+ const handleToggleCampaignWall = async (campaign: Campaign, field: "is_open_for_applications" | "is_on_campaign_wall") => {
+ try {
+ setUpdatingCampaign(campaign.id);
+ const newValue = !campaign[field];
+ await updateCampaign(campaign.id, { [field]: newValue });
+ // Reload campaigns to reflect changes
+ await loadCampaigns();
+ } catch (err: any) {
+ console.error("Failed to update campaign:", err);
+ setError(err.message || "Failed to update campaign");
+ } finally {
+ setUpdatingCampaign(null);
+ }
+ };
+
+ const handleApplicationStatusChange = async (
+ campaignId: string,
+ applicationId: string,
+ newStatus: "reviewing" | "accepted" | "rejected"
+ ) => {
+ try {
+ await updateApplicationStatus(campaignId, applicationId, newStatus);
+ // Reload applications
+ await loadApplications(campaignId);
+ if (newStatus === "accepted") {
+ alert("Application accepted! You can now create a proposal.");
+ }
+ } catch (err: any) {
+ console.error("Failed to update application status:", err);
+ setError(err.message || "Failed to update application status");
+ }
+ };
+
+ const normalizedSearch = searchTerm.trim().toLowerCase();
+ const filteredCampaigns = campaigns.filter((campaign) => {
+ // Filter by open campaigns if enabled
+ if (openCampaignsFilter && !campaign.is_open_for_applications) {
+ return false;
+ }
+ // Filter by search term
+ if (!normalizedSearch) return true;
+ return (
+ campaign.title.toLowerCase().includes(normalizedSearch) ||
+ (campaign.short_description ?? "")
+ .toLowerCase()
+ .includes(normalizedSearch) ||
+ (campaign.description ?? "").toLowerCase().includes(normalizedSearch)
+ );
+ });
+
+ return (
+
+
+
+
+
+ {/* Header */}
+
+
+
My Campaigns
+
+ Manage and track all your campaigns
+
+
+
router.push("/brand/campaigns/create")}
+ className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-purple-500 to-purple-600 px-6 py-3 font-semibold text-white shadow-lg transition-all hover:shadow-xl"
+ >
+
+ Create Campaign
+
+
+
+ {/* Search and Filters */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ onKeyPress={(e) => e.key === "Enter" && handleSearch()}
+ className="w-full rounded-lg border border-gray-300 py-3 pr-4 pl-10 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none"
+ />
+
+
+
+
+
+ {/* Loading State */}
+ {loading && (
+
+
+
Loading campaigns...
+
+ )}
+
+ {/* Error State */}
+ {error && (
+
+
{error}
+
+ Try again
+
+
+ )}
+
+ {/* Empty State */}
+ {!loading && !error && filteredCampaigns.length === 0 && (
+
+
+
+
+
+ No campaigns found
+
+
+ {searchTerm || statusFilter
+ ? "Try adjusting your filters"
+ : "Get started by creating your first campaign"}
+
+ {!searchTerm && !statusFilter && (
+
router.push("/brand/campaigns/create")}
+ className="inline-flex items-center gap-2 rounded-lg bg-purple-500 px-6 py-3 font-semibold text-white transition-colors hover:bg-purple-600"
+ >
+
+ Create Your First Campaign
+
+ )}
+
+ )}
+
+ {/* Campaigns List */}
+ {!loading && !error && filteredCampaigns.length > 0 && (
+
+ {filteredCampaigns.map((campaign) => (
+
+ {/* Campaign Summary */}
+
toggleExpand(campaign.id)}
+ className="cursor-pointer p-6 transition-colors hover:bg-gray-50"
+ >
+
+
+
+
+ {campaign.title}
+
+
+ {campaign.status.toUpperCase()}
+
+
+ {campaign.short_description && (
+
+ {campaign.short_description}
+
+ )}
+
+
+
+
+ Created: {formatDate(campaign.created_at)}
+
+
+ {campaign.budget_min && campaign.budget_max && (
+
+
+
+ {formatCurrency(campaign.budget_min)} -{" "}
+ {formatCurrency(campaign.budget_max)}
+
+
+ )}
+ {campaign.platforms.length > 0 && (
+
+
+ {campaign.platforms.join(", ")}
+
+ )}
+
+
+
+ {expandedCampaign === campaign.id ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Expanded Details */}
+ {expandedCampaign === campaign.id && (
+
+
+ {/* Description */}
+ {campaign.description && (
+
+
+ Description
+
+
+ {campaign.description}
+
+
+ )}
+
+ {/* Dates */}
+ {(campaign.starts_at || campaign.ends_at) && (
+
+
+ Campaign Duration
+
+
+ {campaign.starts_at && (
+
Start: {formatDate(campaign.starts_at)}
+ )}
+ {campaign.ends_at && (
+
End: {formatDate(campaign.ends_at)}
+ )}
+
+
+ )}
+
+ {/* Budget */}
+ {(campaign.budget_min || campaign.budget_max) && (
+
+
+ Budget Range
+
+
+ {campaign.budget_min &&
+ formatCurrency(campaign.budget_min)}
+ {campaign.budget_min &&
+ campaign.budget_max &&
+ " - "}
+ {campaign.budget_max &&
+ formatCurrency(campaign.budget_max)}
+
+
+ )}
+
+ {/* Platforms */}
+ {campaign.platforms.length > 0 && (
+
+
+ Platforms
+
+
+ {campaign.platforms.map((platform) => (
+
+ {platform}
+
+ ))}
+
+
+ )}
+
+ {/* Creator Preferences */}
+ {campaign.preferred_creator_niches.length > 0 && (
+
+
+ Preferred Creator Niches
+
+
+ {campaign.preferred_creator_niches.map(
+ (niche) => (
+
+ {niche}
+
+ )
+ )}
+
+
+ )}
+
+ {/* Follower Range */}
+ {campaign.preferred_creator_followers_range && (
+
+
+ Creator Follower Range
+
+
+ {campaign.preferred_creator_followers_range}
+
+
+ )}
+
+ {/* Deliverables */}
+ {campaign.deliverables &&
+ Array.isArray(campaign.deliverables) &&
+ campaign.deliverables.length > 0 && (
+
+
+ Deliverables
+
+
+ {campaign.deliverables.map(
+ (deliverable, idx) => (
+
+
+
+
+ {deliverable.platform} -{" "}
+ {deliverable.content_type}
+
+
+ (Qty: {deliverable.quantity})
+
+
+ {deliverable.required && (
+
+ Required
+
+ )}
+
+ {deliverable.guidance && (
+
+ {deliverable.guidance}
+
+ )}
+
+ )
+ )}
+
+
+ )}
+
+
+ {/* Campaign Wall Settings */}
+
+
Campaign Wall Settings
+
+
+
+
Open for Applications
+
Allow creators to apply to this campaign
+
+
handleToggleCampaignWall(campaign, "is_open_for_applications")}
+ disabled={updatingCampaign === campaign.id}
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
+ campaign.is_open_for_applications ? "bg-purple-600" : "bg-gray-300"
+ } ${updatingCampaign === campaign.id ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
+ >
+
+
+
+
+
+
Show on Campaign Wall
+
Display this campaign on the public campaign wall
+
+
handleToggleCampaignWall(campaign, "is_on_campaign_wall")}
+ disabled={updatingCampaign === campaign.id || !campaign.is_open_for_applications}
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
+ campaign.is_on_campaign_wall && campaign.is_open_for_applications ? "bg-purple-600" : "bg-gray-300"
+ } ${updatingCampaign === campaign.id || !campaign.is_open_for_applications ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
+ >
+
+
+
+
+
+
+ {/* Applications Section */}
+ {campaign.is_open_for_applications && (
+
+
+
+
+ Applications ({applications[campaign.id]?.length || 0})
+
+ loadApplications(campaign.id)}
+ className="text-sm text-purple-600 hover:text-purple-700"
+ >
+ Refresh
+
+
+ {loadingApplications[campaign.id] ? (
+
+
+
Loading applications...
+
+ ) : applications[campaign.id]?.length > 0 ? (
+
+ {applications[campaign.id].map((app) => (
+
+
+
+ {app.creator_profile_picture ? (
+
+ ) : (
+
+ )}
+
+
{app.creator_name || "Unknown Creator"}
+
{new Date(app.created_at).toLocaleDateString()}
+
+
+
+ {app.status}
+
+
+ {app.description && (
+
{app.description}
+ )}
+
+ {app.payment_min && app.payment_max && (
+
+ Payment:
+
+ ₹{app.payment_min.toLocaleString()} - ₹{app.payment_max.toLocaleString()}
+
+
+ )}
+ {app.timeline_days && (
+
+ Timeline:
+ {app.timeline_days} days
+
+ )}
+ {app.timeline_weeks && (
+
+ Timeline:
+ {app.timeline_weeks} weeks
+
+ )}
+
+ {(app.status === "applied" || app.status === "reviewing") && (
+
+ handleApplicationStatusChange(campaign.id, app.id, "accepted")}
+ className="flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-white bg-green-600 rounded hover:bg-green-700"
+ >
+
+ Accept
+
+ {app.status === "applied" && (
+ handleApplicationStatusChange(campaign.id, app.id, "reviewing")}
+ className="flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded hover:bg-gray-200"
+ >
+
+ Review
+
+ )}
+ handleApplicationStatusChange(campaign.id, app.id, "rejected")}
+ className="flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-white bg-red-600 rounded hover:bg-red-700"
+ >
+
+ Reject
+
+
+ )}
+
+ ))}
+
+ ) : (
+
+
+
No applications yet
+
+ )}
+
+ )}
+
+ {/* Actions */}
+
+
+ router.push(`/brand/campaigns/edit/${campaign.id}`)
+ }
+ className="rounded-lg bg-blue-500 px-6 py-2 font-semibold text-white transition-colors hover:bg-blue-600"
+ >
+ Edit Campaign
+
+
+ router.push(`/brand/campaigns/${campaign.id}/find-creators`)
+ }
+ className="rounded-lg bg-gradient-to-r from-purple-500 to-purple-600 px-6 py-2 font-semibold text-white transition-colors hover:from-purple-600 hover:to-purple-700"
+ >
+ Find Creators
+
+
+
+ )}
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/app/brand/contracts/page.tsx b/frontend/app/brand/contracts/page.tsx
new file mode 100644
index 0000000..b7a04b9
--- /dev/null
+++ b/frontend/app/brand/contracts/page.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import { ContractsWorkspace } from "@/components/contracts/ContractsWorkspace";
+import SlidingMenu from "@/components/SlidingMenu";
+
+export default function BrandContractsPage() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/frontend/app/brand/createcampaign/page.tsx b/frontend/app/brand/createcampaign/page.tsx
new file mode 100644
index 0000000..4b006a4
--- /dev/null
+++ b/frontend/app/brand/createcampaign/page.tsx
@@ -0,0 +1,196 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import { getBrandDashboardStats } from "@/lib/api/analytics";
+import { Eye, Loader2, PlusCircle, Sparkles } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function BrandCreateCampaign() {
+ const router = useRouter();
+ const [stats, setStats] = useState({
+ totalCampaigns: 0,
+ activeCampaigns: 0,
+ applications: 0,
+ });
+ const [statsLoading, setStatsLoading] = useState(true);
+
+ useEffect(() => {
+ loadStats();
+ }, []);
+
+ const loadStats = async () => {
+ try {
+ setStatsLoading(true);
+ const dashboardStats = await getBrandDashboardStats();
+ setStats({
+ totalCampaigns: dashboardStats.overview.total_campaigns,
+ activeCampaigns: dashboardStats.overview.active_campaigns,
+ applications: dashboardStats.overview.total_proposals,
+ });
+ } catch (err) {
+ console.error("Failed to load stats:", err);
+ } finally {
+ setStatsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {/* Header */}
+
+
+
+ Campaign Management
+
+
+ Create new campaigns or manage your existing ones
+
+
+
+ {/* Action Cards */}
+
+ {/* View All Campaigns Card */}
+
router.push("/brand/campaigns")}
+ className="group cursor-pointer overflow-hidden rounded-2xl bg-white shadow-lg transition-all duration-300 hover:scale-105 hover:shadow-2xl"
+ >
+
+
+
View All Campaigns
+
+ Browse, search, and manage your existing campaigns
+
+
+
+
+
+ ✓
+ View all your campaigns in one place
+
+
+ ✓
+ Search and filter by status, date, or name
+
+
+ ✓
+ See campaign performance and details
+
+
+ ✓
+ Track applications and collaborations
+
+
+
+
+ Go to Campaigns
+
+
+
+
+
+ {/* Create New Campaign Card */}
+
router.push("/brand/campaigns/create")}
+ className="group cursor-pointer overflow-hidden rounded-2xl bg-white shadow-lg transition-all duration-300 hover:scale-105 hover:shadow-2xl"
+ >
+
+
+
Create New Campaign
+
+ Launch a new influencer marketing campaign
+
+
+
+
+
+ ✓
+ Set campaign goals and objectives
+
+
+ ✓
+ Define target audience and platforms
+
+
+ ✓
+ Specify budget and deliverables
+
+
+ ✓
+ Find the perfect creators for your brand
+
+
+
+
+ Start Creating
+
+
+
+
+
+
+ {/* Quick Stats */}
+
+
+
+
+ Total Campaigns
+
+ {statsLoading ? (
+
+
+
+ ) : (
+
+ {stats.totalCampaigns}
+
+ )}
+
+
+
+
+
+ Active Campaigns
+
+ {statsLoading ? (
+
+
+
+ ) : (
+
+ {stats.activeCampaigns}
+
+ )}
+
+
+
+
+
+ Applications
+
+ {statsLoading ? (
+
+
+
+ ) : (
+
+ {stats.applications}
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/brand/home/page.tsx b/frontend/app/brand/home/page.tsx
new file mode 100644
index 0000000..9085543
--- /dev/null
+++ b/frontend/app/brand/home/page.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import BrandDashboard from "@/components/dashboard/BrandDashboard";
+import ProfileButton from "@/components/profile/ProfileButton";
+import { getUserProfile, signOut } from "@/lib/auth-helpers";
+import { Briefcase, Loader2, LogOut } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function BrandHomePage() {
+ const router = useRouter();
+ const [userName, setUserName] = useState("");
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+
+ useEffect(() => {
+ async function loadProfile() {
+ const profile = await getUserProfile();
+ if (profile) {
+ setUserName(profile.name);
+ }
+ }
+ loadProfile();
+ }, []);
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+ await signOut();
+ router.push("/login");
+ } catch (error) {
+ console.error("Logout error:", error);
+ setIsLoggingOut(false);
+ }
+ };
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+ InPactAI
+
+
+
+
+
+ {isLoggingOut ? (
+
+ ) : (
+
+ )}
+ {isLoggingOut ? "Logging out..." : "Logout"}
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Welcome Header */}
+
+
+ Welcome back, {userName || "Brand"}!
+
+
+ Here's an overview of your campaigns and performance
+
+
+
+ {/* Dashboard */}
+
+
+
+
+ );
+}
diff --git a/frontend/app/brand/onboarding/page.tsx b/frontend/app/brand/onboarding/page.tsx
new file mode 100644
index 0000000..8933313
--- /dev/null
+++ b/frontend/app/brand/onboarding/page.tsx
@@ -0,0 +1,987 @@
+"use client";
+
+import ImageUpload from "@/components/onboarding/ImageUpload";
+import MultiSelect from "@/components/onboarding/MultiSelect";
+import ProgressBar from "@/components/onboarding/ProgressBar";
+import TypeformQuestion from "@/components/onboarding/TypeformQuestion";
+import { uploadBrandLogo } from "@/lib/storage-helpers";
+import { supabase } from "@/lib/supabaseClient";
+import { AnimatePresence } from "framer-motion";
+import { CheckCircle2, Loader2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+const TOTAL_STEPS = 11;
+
+// Industry options
+const INDUSTRIES = [
+ "Technology & Software",
+ "Fashion & Apparel",
+ "Beauty & Cosmetics",
+ "Food & Beverage",
+ "Health & Wellness",
+ "Gaming & Esports",
+ "Travel & Hospitality",
+ "E-commerce & Retail",
+ "Financial Services",
+ "Education & E-learning",
+ "Entertainment & Media",
+ "Sports & Fitness",
+ "Home & Lifestyle",
+ "Automotive",
+ "B2B Services",
+ "Other",
+];
+
+// Company sizes
+const COMPANY_SIZES = [
+ "Startup (1-10)",
+ "Small (11-50)",
+ "Medium (51-200)",
+ "Large (201-1000)",
+ "Enterprise (1000+)",
+];
+
+// Age groups
+const AGE_GROUPS = ["13-17", "18-24", "25-34", "35-44", "45-54", "55+"];
+
+// Genders
+const GENDERS = ["Male", "Female", "Non-binary", "All"];
+
+// Locations
+const LOCATIONS = [
+ "United States",
+ "India",
+ "United Kingdom",
+ "Canada",
+ "Australia",
+ "Europe",
+ "Asia",
+ "Global",
+ "Other",
+];
+
+// Interests/Niches
+const INTERESTS = [
+ "Gaming",
+ "Technology",
+ "Fashion",
+ "Beauty",
+ "Fitness",
+ "Food",
+ "Travel",
+ "Lifestyle",
+ "Education",
+ "Entertainment",
+ "Business",
+ "Arts",
+ "Music",
+ "Sports",
+ "Parenting",
+ "Home",
+];
+
+// Brand values
+const BRAND_VALUES = [
+ "Sustainability",
+ "Innovation",
+ "Quality",
+ "Affordability",
+ "Inclusivity",
+ "Authenticity",
+ "Transparency",
+ "Social Responsibility",
+ "Customer Focus",
+ "Excellence",
+];
+
+// Brand personality
+const BRAND_PERSONALITIES = [
+ "Professional",
+ "Fun",
+ "Edgy",
+ "Friendly",
+ "Luxurious",
+ "Casual",
+ "Bold",
+ "Sophisticated",
+ "Playful",
+ "Trustworthy",
+];
+
+// Marketing goals
+const MARKETING_GOALS = [
+ "Brand Awareness",
+ "Product Sales",
+ "Lead Generation",
+ "Social Engagement",
+ "Content Creation",
+ "Community Building",
+ "Market Research",
+ "Customer Retention",
+];
+
+// Budget ranges
+const BUDGET_RANGES = [
+ "Less than $5K",
+ "$5K - $20K",
+ "$20K - $50K",
+ "$50K - $100K",
+ "$100K+",
+];
+
+const CAMPAIGN_BUDGET_RANGES = [
+ "Less than $1K",
+ "$1K - $5K",
+ "$5K - $10K",
+ "$10K - $25K",
+ "$25K+",
+];
+
+// Campaign types
+const CAMPAIGN_TYPES = [
+ "Sponsored Posts",
+ "Product Reviews",
+ "Brand Ambassadorships",
+ "Event Coverage",
+ "User Generated Content",
+ "Influencer Takeovers",
+];
+
+// Preferred creator sizes
+const PREFERRED_CREATOR_SIZES = [
+ "Nano (1K-10K)",
+ "Micro (10K-100K)",
+ "Mid-tier (100K-500K)",
+ "Macro (500K-1M)",
+ "Mega (1M+)",
+];
+
+interface BrandFormData {
+ companyName: string;
+ companyTagline: string;
+ industry: string;
+ description: string;
+ websiteUrl: string;
+ companySize: string;
+ targetAgeGroups: string[];
+ targetGenders: string[];
+ targetLocations: string[];
+ targetInterests: string[];
+ brandValues: string[];
+ brandPersonality: string[];
+ marketingGoals: string[];
+ monthlyBudget: string;
+ budgetPerCampaignMin: string;
+ budgetPerCampaignMax: string;
+ campaignTypes: string[];
+ preferredNiches: string[];
+ preferredCreatorSize: string[];
+ minFollowers: string;
+ companyLogo: File | null;
+}
+
+export default function BrandOnboardingPage() {
+ const router = useRouter();
+ const [currentStep, setCurrentStep] = useState(1);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [userId, setUserId] = useState(null);
+
+ const [formData, setFormData] = useState({
+ companyName: "",
+ companyTagline: "",
+ industry: "",
+ description: "",
+ websiteUrl: "",
+ companySize: "",
+ targetAgeGroups: [],
+ targetGenders: [],
+ targetLocations: [],
+ targetInterests: [],
+ brandValues: [],
+ brandPersonality: [],
+ marketingGoals: [],
+ monthlyBudget: "",
+ budgetPerCampaignMin: "",
+ budgetPerCampaignMax: "",
+ campaignTypes: [],
+ preferredNiches: [],
+ preferredCreatorSize: [],
+ minFollowers: "",
+ companyLogo: null,
+ });
+
+ // Get user ID on mount
+ useEffect(() => {
+ const getUser = async () => {
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+ if (!user) {
+ router.push("/login");
+ return;
+ }
+ setUserId(user.id);
+ };
+ getUser();
+ }, [router]);
+
+ const handleNext = () => {
+ if (currentStep < TOTAL_STEPS) {
+ setCurrentStep(currentStep + 1);
+ }
+ };
+
+ const handleBack = () => {
+ if (currentStep > 1) {
+ setCurrentStep(currentStep - 1);
+ }
+ };
+
+ const updateFormData = (field: keyof BrandFormData, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleComplete = async () => {
+ if (!userId) return;
+
+ setIsSubmitting(true);
+
+ try {
+ // Upload company logo if provided
+ let logoUrl = null;
+ if (formData.companyLogo) {
+ logoUrl = await uploadBrandLogo(formData.companyLogo, userId);
+ }
+
+ // Insert brand profile with correct column names
+ const { error: brandError } = await supabase.from("brands").insert({
+ user_id: userId,
+ company_name: formData.companyName,
+ company_tagline: formData.companyTagline,
+ industry: formData.industry,
+ company_description: formData.description,
+ website_url: formData.websiteUrl,
+ company_size: formData.companySize,
+ company_logo_url: logoUrl,
+ target_audience_age_groups: formData.targetAgeGroups,
+ target_audience_gender: formData.targetGenders,
+ target_audience_locations: formData.targetLocations,
+ target_audience_interests: formData.targetInterests,
+ brand_values: formData.brandValues,
+ brand_personality: formData.brandPersonality,
+ marketing_goals: formData.marketingGoals,
+ monthly_marketing_budget: formData.monthlyBudget
+ ? Number(formData.monthlyBudget)
+ : null,
+ budget_per_campaign_min: formData.budgetPerCampaignMin
+ ? Number(formData.budgetPerCampaignMin)
+ : null,
+ budget_per_campaign_max: formData.budgetPerCampaignMax
+ ? Number(formData.budgetPerCampaignMax)
+ : null,
+ campaign_types_interested: formData.campaignTypes,
+ preferred_creator_niches: formData.preferredNiches,
+ preferred_creator_size: formData.preferredCreatorSize,
+ minimum_followers_required: formData.minFollowers
+ ? parseInt(formData.minFollowers)
+ : null,
+ });
+
+ if (brandError) throw brandError;
+
+ // Mark onboarding as complete
+ const { error: profileError } = await supabase
+ .from("profiles")
+ .update({ onboarding_completed: true })
+ .eq("id", userId);
+
+ if (profileError) throw profileError;
+
+ // Show success and redirect
+ setTimeout(() => {
+ router.push("/brand/home");
+ }, 2000);
+ } catch (error: any) {
+ console.error("Onboarding error:", error);
+ alert("Failed to complete onboarding. Please try again.");
+ setIsSubmitting(false);
+ }
+ };
+
+ // Validation for each step
+ const canGoNext = () => {
+ switch (currentStep) {
+ case 1:
+ return true; // Welcome screen
+ case 2:
+ return formData.companyName.trim().length >= 2;
+ case 3:
+ return formData.industry !== "";
+ case 4:
+ return (
+ formData.description.trim().length >= 50 &&
+ formData.websiteUrl.trim().length > 0 &&
+ formData.companySize !== ""
+ );
+ case 5:
+ return (
+ formData.targetAgeGroups.length > 0 &&
+ formData.targetGenders.length > 0 &&
+ formData.targetLocations.length > 0
+ );
+ case 6:
+ return (
+ formData.brandValues.length > 0 &&
+ formData.brandPersonality.length > 0
+ );
+ case 7:
+ return formData.marketingGoals.length > 0;
+ case 8:
+ return (
+ formData.monthlyBudget !== "" &&
+ formData.budgetPerCampaignMin !== "" &&
+ formData.budgetPerCampaignMax !== "" &&
+ formData.campaignTypes.length > 0
+ );
+ case 9:
+ return (
+ formData.preferredNiches.length > 0 &&
+ formData.preferredCreatorSize.length > 0
+ );
+ case 10:
+ return true; // Logo is optional
+ case 11:
+ return true; // Review step
+ default:
+ return false;
+ }
+ };
+
+ if (!userId) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+ {/* Step 1: Welcome */}
+ {currentStep === 1 && (
+
+
+
+ We'll help you create a profile to connect with the perfect
+ creators for your brand. Ready?
+
+
+
+ )}
+
+ {/* Step 2: Company Basics */}
+ {currentStep === 2 && (
+
+
+
+ )}
+
+ {/* Step 3: Industry */}
+ {currentStep === 3 && (
+
+ updateFormData("industry", e.target.value)}
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 text-lg transition-colors focus:border-purple-500 focus:outline-none"
+ >
+ Select your industry
+ {INDUSTRIES.map((industry) => (
+
+ {industry}
+
+ ))}
+
+
+ )}
+
+ {/* Step 4: Company Description */}
+ {currentStep === 4 && (
+
+
+
+
+ Description *
+
+
+ updateFormData("description", e.target.value)
+ }
+ placeholder="What does your company do? What makes you unique?"
+ rows={5}
+ maxLength={500}
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 transition-colors focus:border-purple-500 focus:outline-none"
+ />
+
+ {formData.description.length}/500 characters
+ {formData.description.length < 50 && ` (minimum 50)`}
+
+
+
+
+ Website URL *
+
+ updateFormData("websiteUrl", e.target.value)}
+ placeholder="https://yourcompany.com"
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 transition-colors focus:border-purple-500 focus:outline-none"
+ />
+
+
+
+ Company Size *
+
+
+ updateFormData("companySize", e.target.value)
+ }
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 transition-colors focus:border-purple-500 focus:outline-none"
+ >
+ Select company size
+ {COMPANY_SIZES.map((size) => (
+
+ {size}
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Step 5: Target Audience */}
+ {currentStep === 5 && (
+
+
+
+ updateFormData("targetAgeGroups", selected)
+ }
+ label="Age Groups *"
+ minSelection={1}
+ />
+
+ updateFormData("targetGenders", selected)
+ }
+ label="Gender *"
+ minSelection={1}
+ />
+
+ updateFormData("targetLocations", selected)
+ }
+ label="Locations *"
+ minSelection={1}
+ />
+
+ updateFormData("targetInterests", selected)
+ }
+ label="Target Interests (Optional)"
+ />
+
+
+ )}
+
+ {/* Step 6: Brand Identity */}
+ {currentStep === 6 && (
+
+
+ updateFormData("brandValues", selected)}
+ label="Brand Values *"
+ minSelection={1}
+ />
+
+ updateFormData("brandPersonality", selected)
+ }
+ label="Brand Personality *"
+ minSelection={1}
+ />
+
+
+ )}
+
+ {/* Step 7: Marketing Goals */}
+ {currentStep === 7 && (
+
+
+ updateFormData("marketingGoals", selected)
+ }
+ minSelection={1}
+ />
+
+ )}
+
+ {/* Step 8: Budget & Campaign Info */}
+ {currentStep === 8 && (
+
+
+
+
+ Monthly Marketing Budget (USD) *
+
+
+ updateFormData("monthlyBudget", e.target.value)
+ }
+ placeholder="e.g. 10000"
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 transition-colors focus:border-purple-500 focus:outline-none"
+ />
+
+
+
+ Budget Per Campaign (Min, USD) *
+
+
+ updateFormData("budgetPerCampaignMin", e.target.value)
+ }
+ placeholder="e.g. 1000"
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 transition-colors focus:border-purple-500 focus:outline-none"
+ />
+
+
+
+ Budget Per Campaign (Max, USD) *
+
+
+ updateFormData("budgetPerCampaignMax", e.target.value)
+ }
+ placeholder="e.g. 5000"
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 transition-colors focus:border-purple-500 focus:outline-none"
+ />
+
+
+
+ Campaign Types Interested In *
+
+
+ updateFormData("campaignTypes", selected)
+ }
+ placeholder="Select campaign types"
+ minSelection={1}
+ />
+
+
+
+ )}
+
+ {/* Step 9: Creator Preferences */}
+ {currentStep === 9 && (
+
+
+
+ )}
+
+ {/* Step 10: Company Logo */}
+ {currentStep === 10 && (
+
+ updateFormData("companyLogo", file)}
+ currentImage={formData.companyLogo}
+ label="Company Logo"
+ maxSizeMB={5}
+ />
+
+ )}
+
+ {/* Step 11: Review & Submit */}
+ {currentStep === 11 && (
+
+ {isSubmitting && (
+
+
+
+
+ Setting up your brand profile...
+
+
+
+ Creator Preferences
+
+
+ Niches: {formData.preferredNiches.join(", ")} • Sizes:{" "}
+ {formData.preferredCreatorSize.join(", ")}
+ {formData.minFollowers &&
+ ` • Min Followers: ${formData.minFollowers}`}
+
+
+
+ {formData.companyTagline && (
+
+
Tagline
+
{formData.companyTagline}
+
+ )}
+
+
Industry
+
{formData.industry}
+
+
+
+ Description
+
+
{formData.description}
+
+
+
Website
+
{formData.websiteUrl}
+
+
+
+ Company Size
+
+
{formData.companySize}
+
+
+
+ Target Audience
+
+
+ Ages: {formData.targetAgeGroups.join(", ")} • Genders:{" "}
+ {formData.targetGenders.join(", ")} • Locations:{" "}
+ {formData.targetLocations.join(", ")}
+
+
+
+
+ Brand Identity
+
+
+ Values: {formData.brandValues.join(", ")} • Personality:{" "}
+ {formData.brandPersonality.join(", ")}
+
+
+
+
+ Marketing Goals
+
+
+ {formData.marketingGoals.join(", ")}
+
+
+
+
Budget
+
+ Monthly: {formData.monthlyBudget} • Per Campaign: $
+ {formData.budgetPerCampaignMin} - $
+ {formData.budgetPerCampaignMax}
+
+
+
+
+ Creator Preferences
+
+
+ Niches: {formData.preferredNiches.join(", ")} • Sizes:{" "}
+ {formData.preferredCreatorSize.join(", ")}
+ {formData.minFollowers &&
+ ` • Min Followers: ${formData.minFollowers}`}
+
+
+
+
+ Campaign Types
+
+
+ {formData.campaignTypes.join(", ")}
+
+
+ {formData.companyLogo && (
+
+
+ Company logo added
+
+ )}
+
+ )}
+ {!isSubmitting && (
+
+ {formData.companyTagline && (
+
+
Tagline
+
{formData.companyTagline}
+
+ )}
+
+
Industry
+
{formData.industry}
+
+
+
+ Description
+
+
{formData.description}
+
+
+
Website
+
{formData.websiteUrl}
+
+
+
+ Company Size
+
+
{formData.companySize}
+
+
+
+ Target Audience
+
+
+ Ages: {formData.targetAgeGroups.join(", ")} • Genders:{" "}
+ {formData.targetGenders.join(", ")} • Locations:{" "}
+ {formData.targetLocations.join(", ")}
+
+
+
+
+ Brand Identity
+
+
+ Values: {formData.brandValues.join(", ")} • Personality:{" "}
+ {formData.brandPersonality.join(", ")}
+
+
+
+
+ Marketing Goals
+
+
+ {formData.marketingGoals.join(", ")}
+
+
+
+
Budget
+
+ Monthly: {formData.monthlyBudget} • Per Campaign: $
+ {formData.budgetPerCampaignMin} - $
+ {formData.budgetPerCampaignMax}
+
+
+
+
+ Creator Preferences
+
+
+ Niches: {formData.preferredNiches.join(", ")} • Sizes:{" "}
+ {formData.preferredCreatorSize.join(", ")}
+ {formData.minFollowers &&
+ ` • Min Followers: ${formData.minFollowers}`}
+
+
+
+
+ Campaign Types
+
+
+ {formData.campaignTypes.join(", ")}
+
+
+ {formData.companyLogo && (
+
+
+ Company logo added
+
+ )}
+
+ )}
+
+ )}
+
+ >
+ );
+}
diff --git a/frontend/app/brand/profile/page.tsx b/frontend/app/brand/profile/page.tsx
new file mode 100644
index 0000000..3e103e5
--- /dev/null
+++ b/frontend/app/brand/profile/page.tsx
@@ -0,0 +1,1314 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import ArrayInput from "@/components/profile/ArrayInput";
+import CollapsibleSection from "@/components/profile/CollapsibleSection";
+import JsonInput from "@/components/profile/JsonInput";
+import ProfileButton from "@/components/profile/ProfileButton";
+import SlidingMenu from "@/components/SlidingMenu";
+import {
+ aiFillBrandProfile,
+ BrandProfile,
+ getBrandProfile,
+ updateBrandProfile,
+} from "@/lib/api/profile";
+import { signOut } from "@/lib/auth-helpers";
+import {
+ ArrowLeft,
+ Briefcase,
+ Edit2,
+ Loader2,
+ LogOut,
+ Save,
+ Sparkles,
+ X,
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function BrandProfilePage() {
+ const router = useRouter();
+ const [profile, setProfile] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [formData, setFormData] = useState>({});
+ const [aiLoading, setAiLoading] = useState(false);
+ const [aiInput, setAiInput] = useState("");
+ const [showAiModal, setShowAiModal] = useState(false);
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+
+ useEffect(() => {
+ loadProfile();
+ }, []);
+
+ const loadProfile = async () => {
+ try {
+ setLoading(true);
+ const data = await getBrandProfile();
+ setProfile(data);
+ setFormData(data);
+ } catch (error) {
+ console.error("Error loading profile:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSave = async () => {
+ try {
+ setSaving(true);
+ const updated = await updateBrandProfile(formData);
+ setProfile(updated);
+ setFormData(updated);
+ setIsEditing(false);
+ } catch (error) {
+ console.error("Error saving profile:", error);
+ alert("Failed to save profile. Please try again.");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (profile) {
+ setFormData(profile);
+ setIsEditing(false);
+ }
+ };
+
+ const handleAiFill = async () => {
+ if (!aiInput.trim()) {
+ alert("Please provide some information about your brand");
+ return;
+ }
+
+ try {
+ setAiLoading(true);
+ const result = await aiFillBrandProfile(aiInput);
+
+ if (!result.data || Object.keys(result.data).length === 0) {
+ alert(
+ result.message ||
+ "No new data could be extracted from your input. Please provide more specific information."
+ );
+ return;
+ }
+
+ // Merge AI-generated data into form, handling all data types properly
+ setFormData((prev) => {
+ const updated = { ...prev };
+ for (const [key, value] of Object.entries(result.data)) {
+ // Properly handle arrays, objects, and primitives
+ if (Array.isArray(value)) {
+ updated[key] = value;
+ } else if (typeof value === "object" && value !== null) {
+ updated[key] = value;
+ } else {
+ updated[key] = value;
+ }
+ }
+ return updated;
+ });
+
+ setAiInput("");
+ setShowAiModal(false);
+
+ // Auto-enable edit mode if not already
+ if (!isEditing) {
+ setIsEditing(true);
+ }
+
+ // Show success message
+ const fieldCount = Object.keys(result.data).length;
+ alert(
+ `Success! ${fieldCount} field${fieldCount !== 1 ? "s" : ""} ${fieldCount !== 1 ? "were" : "was"} filled. Please review and save your changes.`
+ );
+ } catch (error: any) {
+ console.error("Error with AI fill:", error);
+ const errorMessage =
+ error?.message || "Failed to generate profile data. Please try again.";
+ alert(errorMessage);
+ } finally {
+ setAiLoading(false);
+ }
+ };
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+ await signOut();
+ router.push("/login");
+ } catch (error) {
+ console.error("Logout error:", error);
+ setIsLoggingOut(false);
+ }
+ };
+
+ const updateField = (field: string, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const updateArrayField = (field: string, values: string[]) => {
+ setFormData((prev) => ({ ...prev, [field]: values }));
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (!profile) {
+ return (
+
+
+
Failed to load profile
+
+
+ );
+ }
+
+ const completionPercentage = profile.profile_completion_percentage || 0;
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+ InPactAI
+
+
+
+
+
+ {isLoggingOut ? (
+
+ ) : (
+
+ )}
+ {isLoggingOut ? "Logging out..." : "Logout"}
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Back Button */}
+ router.push("/brand/home")}
+ className="mb-6 flex items-center gap-2 text-gray-600 hover:text-gray-900"
+ >
+
+ Back to Home
+
+
+ {/* Profile Header */}
+
+
+
+ Brand Profile
+
+
+ {!isEditing ? (
+ <>
+ setShowAiModal(true)}
+ className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-purple-600 to-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:shadow-lg"
+ >
+
+ AI Fill
+
+ setIsEditing(true)}
+ className="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-700"
+ >
+
+ Edit
+
+ >
+ ) : (
+ <>
+
+
+ Cancel
+
+
+ {saving ? (
+
+ ) : (
+
+ )}
+ {saving ? "Saving..." : "Save"}
+
+ >
+ )}
+
+
+
+ {/* Completion Bar */}
+
+
+
+ Profile Completion
+
+
+ {completionPercentage}%
+
+
+
+
+
+
+ {/* Profile Form */}
+
+ {/* Basic Information */}
+
+
+
+
+ Company Name *
+
+
+ updateField("company_name", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Industry *
+
+ updateField("industry", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Sub Industries
+
+
+ updateArrayField("sub_industry", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ Company Tagline
+
+
+ updateField("company_tagline", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Company Type
+
+
+ updateField("company_type", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., B2B, B2C, SaaS"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Website URL *
+
+ updateField("website_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Company Size
+
+
+ updateField("company_size", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., 1-10, 11-50, 51-200, 201-500, 500+"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Founded Year
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "founded_year",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Headquarters Location
+
+
+ updateField("headquarters_location", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Contact Email
+
+
+ updateField("contact_email", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Contact Phone
+
+
+ updateField("contact_phone", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Company Description
+
+
+ updateField("company_description", e.target.value)
+ }
+ disabled={!isEditing}
+ rows={4}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Company Logo URL
+
+
+ updateField("company_logo_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Company Cover Image URL
+
+
+ updateField("company_cover_image_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("social_media_links", value)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Brand Identity */}
+
+
+
+
+ updateArrayField("brand_values", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("brand_personality", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ Brand Voice
+
+ updateField("brand_voice", e.target.value)}
+ disabled={!isEditing}
+ rows={3}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+ updateField("brand_colors", value)}
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Target Audience */}
+
+
+
+
+ Target Audience Description
+
+
+ updateField("target_audience_description", e.target.value)
+ }
+ disabled={!isEditing}
+ rows={3}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateArrayField("target_audience_age_groups", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("target_audience_gender", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("target_audience_locations", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("target_audience_interests", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("target_audience_income_level", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Marketing & Campaigns */}
+
+
+
+
+ updateArrayField("marketing_goals", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("campaign_types_interested", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_content_types", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_platforms", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ Campaign Frequency
+
+
+ updateField("campaign_frequency", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., Monthly, Quarterly"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Monthly Marketing Budget
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "monthly_marketing_budget",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Influencer Budget Percentage
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "influencer_budget_percentage",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Budget per Campaign (Min)
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "budget_per_campaign_min",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Budget per Campaign (Max)
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "budget_per_campaign_max",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Typical Deal Size
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "typical_deal_size",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Payment Terms
+
+
+ updateField("payment_terms", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ {/* Creator Preferences */}
+
+
+
+
+ updateArrayField("preferred_creator_niches", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_creator_size", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_creator_locations", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ Minimum Followers Required
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "minimum_followers_required",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Minimum Engagement Rate (%)
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "minimum_engagement_rate",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField(
+ "offers_product_only_deals",
+ e.target.checked
+ )
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+ Offers Product-Only Deals
+
+
+
+
+
+
+ updateField(
+ "offers_affiliate_programs",
+ e.target.checked
+ )
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+ Offers Affiliate Programs
+
+
+
+
+
+ Affiliate Commission Rate (%)
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "affiliate_commission_rate",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ {/* Content Guidelines */}
+
+
+
+
+ updateArrayField("content_dos", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("content_donts", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("brand_safety_requirements", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("competitor_brands", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+
+ {/* Products & Services */}
+
+
+
+
+ updateArrayField("products_services", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ Product Price Range
+
+
+ updateField("product_price_range", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., $10-$50, $50-$100"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateArrayField("product_categories", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ updateField("seasonal_products", e.target.checked)
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+ Seasonal Products
+
+
+
+
+
+ Product Catalog URL
+
+
+ updateField("product_catalog_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ {/* History & Performance */}
+
+
+
+
+ {/* Additional Settings */}
+
+
+
+
+ updateArrayField("search_keywords", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ Subscription Tier
+
+
+ updateField("subscription_tier", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ >
+ Free
+ Basic
+ Premium
+ Enterprise
+
+
+
+
+ Matching Score Base
+
+
+ updateField(
+ "matching_score_base",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ AI Profile Summary
+
+
+ updateField("ai_profile_summary", e.target.value)
+ }
+ disabled={!isEditing}
+ rows={4}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+
+
+ {/* AI Fill Modal */}
+ {showAiModal && (
+
+
+
+ AI Profile Filling
+
+
+ Provide information about your brand, and AI will help fill in
+ your profile fields automatically.
+
+
setAiInput(e.target.value)}
+ placeholder="e.g., We are a tech startup founded in 2020, based in San Francisco. We focus on sustainable products and target millennials..."
+ rows={6}
+ className="mb-4 w-full rounded-lg border border-gray-300 px-3 py-2"
+ />
+
+ {
+ setShowAiModal(false);
+ setAiInput("");
+ }}
+ className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+
+ {aiLoading ? (
+ <>
+
+ Generating...
+ >
+ ) : (
+ <>
+
+ Generate
+ >
+ )}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/app/brand/proposals/page.tsx b/frontend/app/brand/proposals/page.tsx
new file mode 100644
index 0000000..b40187c
--- /dev/null
+++ b/frontend/app/brand/proposals/page.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import {
+ ProposalsWorkspace,
+ TabKey,
+} from "@/components/proposals/ProposalsWorkspace";
+import { useSearchParams } from "next/navigation";
+import { Suspense, useMemo } from "react";
+
+export default function BrandProposalsPage() {
+ return (
+
+ Loading proposals...
+
+ }
+ >
+
+
+ );
+}
+
+function BrandProposalsContent() {
+ const searchParams = useSearchParams();
+
+ const initialTab = useMemo(() => {
+ const section = searchParams?.get("section")?.toLowerCase();
+ if (section === "negotiations") {
+ return section as TabKey;
+ }
+ return "proposals";
+ }, [searchParams]);
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/app/creator/analytics/[campaign_id]/page.tsx b/frontend/app/creator/analytics/[campaign_id]/page.tsx
new file mode 100644
index 0000000..92dd9f7
--- /dev/null
+++ b/frontend/app/creator/analytics/[campaign_id]/page.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import { use } from "react";
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import CreatorCampaignDetailsView from "@/components/analytics/CreatorCampaignDetailsView";
+
+export default function CreatorCampaignDetailsPage({
+ params,
+}: {
+ params: Promise<{ campaign_id: string }>;
+}) {
+ const { campaign_id } = use(params);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/frontend/app/creator/analytics/page.tsx b/frontend/app/creator/analytics/page.tsx
new file mode 100644
index 0000000..693e388
--- /dev/null
+++ b/frontend/app/creator/analytics/page.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import CreatorAnalyticsDashboard from "@/components/analytics/CreatorAnalyticsDashboard";
+import AIAnalyticsDashboard from "@/components/analytics/AIAnalyticsDashboard";
+import { useState } from "react";
+
+export default function CreatorAnalyticsPage() {
+ const [activeView, setActiveView] = useState<"standard" | "ai">("ai");
+
+ return (
+
+
+
+
+
+ setActiveView("ai")}
+ className={`px-4 py-2 rounded-lg font-medium ${
+ activeView === "ai"
+ ? "bg-purple-600 text-white"
+ : "bg-white text-gray-700 hover:bg-gray-50"
+ }`}
+ >
+ AI Analytics
+
+ setActiveView("standard")}
+ className={`px-4 py-2 rounded-lg font-medium ${
+ activeView === "standard"
+ ? "bg-purple-600 text-white"
+ : "bg-white text-gray-700 hover:bg-gray-50"
+ }`}
+ >
+ Standard Analytics
+
+
+ {activeView === "ai" ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/app/creator/campaign-wall/page.tsx b/frontend/app/creator/campaign-wall/page.tsx
new file mode 100644
index 0000000..7dded57
--- /dev/null
+++ b/frontend/app/creator/campaign-wall/page.tsx
@@ -0,0 +1,731 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import {
+ fetchPublicCampaigns,
+ fetchCampaignRecommendations,
+ createCampaignApplication,
+ fetchCreatorApplications,
+ type CampaignApplication,
+ type CampaignApplicationCreate,
+} from "@/lib/api/campaignWall";
+import { createProposal } from "@/lib/api/proposals";
+import { Campaign, PLATFORM_OPTIONS, NICHE_OPTIONS } from "@/types/campaign";
+import {
+ Search,
+ Filter,
+ Sparkles,
+ DollarSign,
+ Calendar,
+ Users,
+ Target,
+ Check,
+ Clock,
+ X,
+ Send,
+ Loader2,
+} from "lucide-react";
+import { useEffect, useState } from "react";
+import { formatCurrency, formatDate } from "@/lib/campaignApi";
+
+export default function CampaignWallPage() {
+ const [activeTab, setActiveTab] = useState<"browse" | "my-applications">("browse");
+ const [campaigns, setCampaigns] = useState([]);
+ const [myApplications, setMyApplications] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [loadingApplications, setLoadingApplications] = useState(false);
+ const [error, setError] = useState(null);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [platformFilter, setPlatformFilter] = useState("");
+ const [nicheFilter, setNicheFilter] = useState("");
+ const [budgetMin, setBudgetMin] = useState("");
+ const [budgetMax, setBudgetMax] = useState("");
+ const [showApplicationModal, setShowApplicationModal] = useState(false);
+ const [selectedCampaign, setSelectedCampaign] = useState(null);
+ const [submittingApplication, setSubmittingApplication] = useState(false);
+ const [applicationData, setApplicationData] = useState({
+ payment_min: undefined,
+ payment_max: undefined,
+ timeline_days: undefined,
+ timeline_weeks: undefined,
+ description: "",
+ });
+ const [loadingRecommendations, setLoadingRecommendations] = useState(false);
+ const [showProposalModal, setShowProposalModal] = useState(false);
+ const [selectedApplication, setSelectedApplication] = useState(null);
+ const [submittingProposal, setSubmittingProposal] = useState(false);
+ const [proposalData, setProposalData] = useState({
+ subject: "",
+ message: "",
+ proposed_amount: "",
+ content_ideas: "",
+ ideal_pricing: "",
+ });
+
+ useEffect(() => {
+ if (activeTab === "browse") {
+ loadCampaigns();
+ } else {
+ loadMyApplications();
+ }
+ }, [activeTab]);
+
+ const loadCampaigns = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const data = await fetchPublicCampaigns({
+ search: searchTerm || undefined,
+ platform: platformFilter || undefined,
+ niche: nicheFilter || undefined,
+ budget_min: budgetMin ? parseFloat(budgetMin) : undefined,
+ budget_max: budgetMax ? parseFloat(budgetMax) : undefined,
+ });
+ setCampaigns(data);
+ } catch (err: any) {
+ setError(err.message || "Failed to load campaigns");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const loadMyApplications = async () => {
+ try {
+ setLoadingApplications(true);
+ setError(null);
+ const data = await fetchCreatorApplications();
+ setMyApplications(data);
+ } catch (err: any) {
+ setError(err.message || "Failed to load applications");
+ } finally {
+ setLoadingApplications(false);
+ }
+ };
+
+ const loadRecommendations = async () => {
+ try {
+ setLoadingRecommendations(true);
+ setError(null);
+ const data = await fetchCampaignRecommendations({ limit: 20, use_ai: true });
+ setCampaigns(data);
+ } catch (err: any) {
+ setError(err.message || "Failed to load recommendations");
+ } finally {
+ setLoadingRecommendations(false);
+ }
+ };
+
+ const handleSearch = () => {
+ loadCampaigns();
+ };
+
+ const handleOpenApplicationModal = (campaign: Campaign) => {
+ setSelectedCampaign(campaign);
+ setApplicationData({
+ payment_min: campaign.budget_min || undefined,
+ payment_max: campaign.budget_max || undefined,
+ timeline_days: undefined,
+ timeline_weeks: undefined,
+ description: "",
+ });
+ setShowApplicationModal(true);
+ };
+
+ const handleSubmitApplication = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!selectedCampaign || !applicationData.description.trim()) {
+ alert("Please fill in all required fields");
+ return;
+ }
+
+ try {
+ setSubmittingApplication(true);
+ setError(null);
+ await createCampaignApplication(selectedCampaign.id, applicationData);
+ alert("Application submitted successfully!");
+ setShowApplicationModal(false);
+ setSelectedCampaign(null);
+ setApplicationData({
+ payment_min: undefined,
+ payment_max: undefined,
+ timeline_days: undefined,
+ timeline_weeks: undefined,
+ description: "",
+ });
+ // Reload campaigns to update applied status
+ await loadCampaigns();
+ await loadMyApplications();
+ } catch (err: any) {
+ setError(err.message || "Failed to submit application");
+ alert(err.message || "Failed to submit application");
+ } finally {
+ setSubmittingApplication(false);
+ }
+ };
+
+ const handleOpenProposalModal = (app: CampaignApplication) => {
+ setSelectedApplication(app);
+ setProposalData({
+ subject: `Proposal for ${app.campaign_title || "Campaign"}`,
+ message: app.description || "",
+ proposed_amount: app.payment_max?.toString() || app.payment_min?.toString() || "",
+ content_ideas: "",
+ ideal_pricing: "",
+ });
+ setShowProposalModal(true);
+ };
+
+ const handleSubmitProposal = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!selectedApplication || !proposalData.message.trim()) {
+ alert("Please fill in all required fields");
+ return;
+ }
+
+ try {
+ setSubmittingProposal(true);
+ setError(null);
+
+ // Use the standard proposal creation endpoint
+ await createProposal({
+ campaign_id: selectedApplication.campaign_id,
+ subject: proposalData.subject || `Proposal for ${selectedApplication.campaign_title || "Campaign"}`,
+ message: proposalData.message,
+ proposed_amount: proposalData.proposed_amount ? parseFloat(proposalData.proposed_amount) : undefined,
+ content_ideas: proposalData.content_ideas ? [proposalData.content_ideas] : [],
+ ideal_pricing: proposalData.ideal_pricing || undefined,
+ });
+
+ alert("Proposal sent successfully!");
+ setShowProposalModal(false);
+ setSelectedApplication(null);
+ setProposalData({
+ subject: "",
+ message: "",
+ proposed_amount: "",
+ content_ideas: "",
+ ideal_pricing: "",
+ });
+ // Reload applications
+ await loadMyApplications();
+ } catch (err: any) {
+ setError(err.message || "Failed to send proposal");
+ alert(err.message || "Failed to send proposal");
+ } finally {
+ setSubmittingProposal(false);
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case "accepted":
+ return "bg-green-100 text-green-800";
+ case "rejected":
+ return "bg-red-100 text-red-800";
+ case "reviewing":
+ return "bg-yellow-100 text-yellow-800";
+ default:
+ return "bg-blue-100 text-blue-800";
+ }
+ };
+
+ const hasApplied = (campaignId: string) => {
+ return myApplications.some((app) => app.campaign_id === campaignId);
+ };
+
+ return (
+
+
+
+
+
+ {/* Header */}
+
+
Campaign Wall
+
Browse and apply to open campaigns
+
+
+ {/* Tabs */}
+
+ setActiveTab("browse")}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
+ activeTab === "browse"
+ ? "border-purple-600 text-purple-600"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ }`}
+ >
+ Browse Campaigns
+
+ setActiveTab("my-applications")}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
+ activeTab === "my-applications"
+ ? "border-purple-600 text-purple-600"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ }`}
+ >
+ My Applications ({myApplications.length})
+
+
+
+ {/* Browse Tab */}
+ {activeTab === "browse" && (
+ <>
+ {/* Search and Filters */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ onKeyPress={(e) => e.key === "Enter" && handleSearch()}
+ className="w-full rounded-lg border text-black border-gray-300 py-3 pr-4 pl-10 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ />
+
+
+
+
+
+ setPlatformFilter(e.target.value)}
+ className="min-w-[120px] appearance-none text-gray-500 rounded-lg border border-gray-300 py-3 pr-10 pl-10 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ >
+ All Platforms
+ {PLATFORM_OPTIONS.map((option) => (
+
+ {option}
+
+ ))}
+
+
+
setNicheFilter(e.target.value)}
+ className="min-w-[120px] appearance-none rounded-lg border text-gray-500 border-gray-300 py-3 pr-10 pl-4 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ >
+ All Niches
+ {NICHE_OPTIONS.map((option) => (
+
+ {option}
+
+ ))}
+
+
setBudgetMin(e.target.value)}
+ className="w-34 rounded-lg border text-black border-gray-300 px-3 py-3 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ min={0}
+ />
+
setBudgetMax(e.target.value)}
+ className="w-34 rounded-lg border text-black border-gray-300 px-3 py-3 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ min={0}
+ />
+
+ Search
+
+
+ {loadingRecommendations ? (
+
+ ) : (
+
+ )}
+ AI Recommendations
+
+
+
+
+ {/* Error State */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Loading State */}
+ {loading && (
+
+
+
Loading campaigns...
+
+ )}
+
+ {/* Campaigns Grid */}
+ {!loading && campaigns.length > 0 && (
+
+ {campaigns.map((campaign) => {
+ const applied = hasApplied(campaign.id);
+ return (
+
+
+
+
{campaign.title}
+ {applied && (
+
+ Applied
+
+ )}
+
+ {campaign.short_description && (
+
{campaign.short_description}
+ )}
+
+ {campaign.budget_min && campaign.budget_max && (
+
+
+
+ {formatCurrency(campaign.budget_min)} - {formatCurrency(campaign.budget_max)}
+
+
+ )}
+ {campaign.platforms.length > 0 && (
+
+
+ {campaign.platforms.join(", ")}
+
+ )}
+ {campaign.preferred_creator_niches.length > 0 && (
+
+
+ {campaign.preferred_creator_niches.join(", ")}
+
+ )}
+
+
handleOpenApplicationModal(campaign)}
+ disabled={applied}
+ className={`w-full rounded-lg px-4 py-2 font-semibold text-white transition-colors ${
+ applied
+ ? "bg-gray-400 cursor-not-allowed"
+ : "bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700"
+ }`}
+ >
+ {applied ? "Already Applied" : "Apply Now"}
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* Empty State */}
+ {!loading && campaigns.length === 0 && (
+
+
+
No campaigns found
+
Try adjusting your filters or check back later
+
+ )}
+ >
+ )}
+
+ {/* My Applications Tab */}
+ {activeTab === "my-applications" && (
+ <>
+ {loadingApplications ? (
+
+
+
Loading applications...
+
+ ) : myApplications.length > 0 ? (
+
+ {myApplications.map((app) => (
+
+
+
+
{app.campaign_title || "Campaign"}
+
+ Applied on {new Date(app.created_at).toLocaleDateString()}
+
+
+
+ {app.status}
+
+
+ {app.description && (
+
{app.description}
+ )}
+
+ {app.payment_min && app.payment_max && (
+
+ Payment:
+
+ ₹{app.payment_min.toLocaleString()} - ₹{app.payment_max.toLocaleString()}
+
+
+ )}
+ {app.timeline_days && (
+
+ Timeline:
+ {app.timeline_days} days
+
+ )}
+ {app.timeline_weeks && (
+
+ Timeline:
+ {app.timeline_weeks} weeks
+
+ )}
+
+ {app.status === "accepted" && (
+
+ handleOpenProposalModal(app)}
+ className="w-full rounded-lg bg-gradient-to-r from-purple-500 to-purple-600 px-4 py-2 font-semibold text-white transition-colors hover:from-purple-600 hover:to-purple-700"
+ >
+ Create & Send Proposal
+
+
+ )}
+
+ ))}
+
+ ) : (
+
+
+
No applications yet
+
Start applying to campaigns to see them here
+
+ )}
+ >
+ )}
+
+ {/* Application Modal */}
+ {showApplicationModal && selectedCampaign && (
+
+
+
+
Apply to Campaign
+
{selectedCampaign.title}
+
+
+
+
+
+
+ Why should you be chosen? *
+
+
+ setApplicationData({ ...applicationData, description: e.target.value })
+ }
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ rows={6}
+ placeholder="Describe your experience, why you're a good fit, and any relevant details..."
+ required
+ />
+
+
+
+ {submittingApplication ? "Submitting..." : "Submit Application"}
+
+ {
+ setShowApplicationModal(false);
+ setSelectedCampaign(null);
+ }}
+ className="rounded-lg border border-gray-300 bg-white px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-50"
+ >
+ Cancel
+
+
+
+
+
+ )}
+
+ {/* Proposal Modal */}
+ {showProposalModal && selectedApplication && (
+
+
+
+
Create Proposal
+
Send a proposal for {selectedApplication.campaign_title || "Campaign"}
+
+
+
+
+ Subject *
+
+ setProposalData({ ...proposalData, subject: e.target.value })}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ required
+ placeholder="Proposal subject..."
+ />
+
+
+
+ Message *
+
+ setProposalData({ ...proposalData, message: e.target.value })}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ rows={6}
+ placeholder="Write your proposal message..."
+ required
+ />
+
+
+
+ Proposed Amount (INR)
+
+ setProposalData({ ...proposalData, proposed_amount: e.target.value })}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ placeholder="Amount"
+ min={0}
+ />
+
+
+
+ Content Ideas
+
+ setProposalData({ ...proposalData, content_ideas: e.target.value })}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ rows={3}
+ placeholder="Describe your content ideas..."
+ />
+
+
+
+ Ideal Pricing
+
+ setProposalData({ ...proposalData, ideal_pricing: e.target.value })}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
+ placeholder="Describe your ideal pricing structure..."
+ />
+
+
+
+ {submittingProposal ? "Sending..." : "Send Proposal"}
+
+ {
+ setShowProposalModal(false);
+ setSelectedApplication(null);
+ }}
+ className="rounded-lg border border-gray-300 bg-white px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-50"
+ >
+ Cancel
+
+
+
+
+
+ )}
+
+
+
+ );
+}
+
diff --git a/frontend/app/creator/collaborations/[collaboration_id]/page.tsx b/frontend/app/creator/collaborations/[collaboration_id]/page.tsx
new file mode 100644
index 0000000..5fcc133
--- /dev/null
+++ b/frontend/app/creator/collaborations/[collaboration_id]/page.tsx
@@ -0,0 +1,815 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import {
+ getCollaborationWorkspace,
+ createDeliverable,
+ updateDeliverable,
+ sendMessage,
+ uploadAsset,
+ completeCollaboration,
+ submitFeedback,
+ type CollaborationWorkspace,
+ type CollaborationDeliverable,
+ type CollaborationMessage,
+} from "@/lib/api/collaborations";
+import { getUserProfile } from "@/lib/auth-helpers";
+import {
+ ArrowLeft,
+ Check,
+ Clock,
+ FileText,
+ Link as LinkIcon,
+ MessageSquare,
+ Plus,
+ Send,
+ Sparkles,
+ Star,
+ Upload,
+ X,
+} from "lucide-react";
+import { useRouter, useParams } from "next/navigation";
+import { useEffect, useState, useRef } from "react";
+
+export default function CollaborationWorkspacePage() {
+ const router = useRouter();
+ const params = useParams<{ collaboration_id?: string | string[] }>();
+ const collaborationIdValue = Array.isArray(params?.collaboration_id)
+ ? params?.collaboration_id[0]
+ : params?.collaboration_id;
+ const collaborationId = collaborationIdValue ?? "";
+
+ const [workspace, setWorkspace] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [activeTab, setActiveTab] = useState<"overview" | "deliverables" | "messages" | "assets">("overview");
+ const [currentUserId, setCurrentUserId] = useState(null);
+
+ // Deliverables state
+ const [showAddDeliverable, setShowAddDeliverable] = useState(false);
+ const [newDeliverable, setNewDeliverable] = useState({
+ description: "",
+ due_date: "",
+ });
+
+ // Messages state
+ const [messageText, setMessageText] = useState("");
+ const messagesEndRef = useRef(null);
+
+ // Assets state
+ const [showAddAsset, setShowAddAsset] = useState(false);
+ const [newAsset, setNewAsset] = useState({
+ url: "",
+ type: "",
+ });
+
+ // Feedback state
+ const [showFeedback, setShowFeedback] = useState(false);
+ const [feedback, setFeedback] = useState({ rating: 5, feedback: "" });
+
+ useEffect(() => {
+ loadCurrentUser();
+ }, []);
+
+ useEffect(() => {
+ if (!collaborationId) return;
+ loadWorkspace();
+ }, [collaborationId]);
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [workspace?.messages]);
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ const loadCurrentUser = async () => {
+ try {
+ const profile = await getUserProfile();
+ if (profile?.id) {
+ setCurrentUserId(profile.id);
+ }
+ } catch (err) {
+ console.error("Failed to load user profile:", err);
+ }
+ };
+
+ const loadWorkspace = async () => {
+ if (!collaborationId) {
+ setError("Collaboration ID is missing. Please return and try again.");
+ return;
+ }
+ try {
+ setIsLoading(true);
+ setError(null);
+ const data = await getCollaborationWorkspace(collaborationId);
+ setWorkspace(data);
+ } catch (err: any) {
+ console.error("Failed to load workspace:", err);
+ setError(err?.message || "Failed to load workspace. Please try again.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const ensureCollaborationId = () => {
+ if (!collaborationId) {
+ setError("Collaboration ID is missing. Please return and try again.");
+ return false;
+ }
+ return true;
+ };
+
+ const handleCreateDeliverable = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!ensureCollaborationId()) return;
+ try {
+ setError(null);
+ await createDeliverable(collaborationId, {
+ description: newDeliverable.description,
+ due_date: newDeliverable.due_date || undefined,
+ });
+ setShowAddDeliverable(false);
+ setNewDeliverable({
+ description: "",
+ due_date: "",
+ });
+ await loadWorkspace();
+ } catch (err: any) {
+ console.error("Failed to create deliverable:", err);
+ setError(err?.message || "Failed to create deliverable. Please try again.");
+ }
+ };
+
+ const handleUpdateDeliverableStatus = async (deliverableId: string, status: string) => {
+ if (!ensureCollaborationId()) return;
+ try {
+ setError(null);
+ await updateDeliverable(collaborationId, deliverableId, { status });
+ await loadWorkspace();
+ } catch (err: any) {
+ console.error("Failed to update deliverable:", err);
+ setError(err?.message || "Failed to update deliverable. Please try again.");
+ }
+ };
+
+ const handleSendMessage = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!messageText.trim()) return;
+ if (!ensureCollaborationId()) return;
+
+ try {
+ setError(null);
+ await sendMessage(collaborationId, { message: messageText });
+ setMessageText("");
+ await loadWorkspace();
+ } catch (err: any) {
+ console.error("Failed to send message:", err);
+ setError(err?.message || "Failed to send message. Please try again.");
+ }
+ };
+
+ const handleUploadAsset = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!ensureCollaborationId()) return;
+ try {
+ setError(null);
+ await uploadAsset(collaborationId, {
+ url: newAsset.url,
+ type: newAsset.type || undefined,
+ });
+ setShowAddAsset(false);
+ setNewAsset({
+ url: "",
+ type: "",
+ });
+ await loadWorkspace();
+ } catch (err: any) {
+ console.error("Failed to upload asset:", err);
+ setError(err?.message || "Failed to upload asset. Please try again.");
+ }
+ };
+
+ const handleComplete = async () => {
+ if (!confirm("Are you sure all deliverables are complete? This will mark the collaboration as completed.")) {
+ return;
+ }
+ if (!ensureCollaborationId()) return;
+ try {
+ setError(null);
+ await completeCollaboration(collaborationId);
+ await loadWorkspace();
+ } catch (err: any) {
+ console.error("Failed to complete collaboration:", err);
+ setError(err?.message || "Failed to complete collaboration. Please try again.");
+ }
+ };
+
+ const handleSubmitFeedback = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!ensureCollaborationId()) return;
+ try {
+ setError(null);
+ await submitFeedback(collaborationId, feedback);
+ setShowFeedback(false);
+ setFeedback({ rating: 5, feedback: "" });
+ await loadWorkspace();
+ } catch (err: any) {
+ console.error("Failed to submit feedback:", err);
+ setError(err?.message || "Failed to submit feedback. Please try again.");
+ }
+ };
+
+ const getStatusColor = (status: string): string => {
+ const colors: Record = {
+ pending: "bg-yellow-100 text-yellow-800",
+ in_progress: "bg-blue-100 text-blue-800",
+ completed: "bg-green-100 text-green-800",
+ approved: "bg-purple-100 text-purple-800",
+ };
+ return colors[status] || "bg-gray-100 text-gray-800";
+ };
+
+ const formatDate = (dateString: string | null): string => {
+ if (!dateString) return "N/A";
+ return new Date(dateString).toLocaleDateString();
+ };
+
+ const formatDateTime = (dateString: string | null): string => {
+ if (!dateString) return "N/A";
+ return new Date(dateString).toLocaleString();
+ };
+
+ const isMyMessage = (message: CollaborationMessage): boolean => {
+ return message.sender_id === currentUserId;
+ };
+
+ const canComplete = (): boolean => {
+ if (!workspace) return false;
+ if (workspace.collaboration.status !== "accepted" && workspace.collaboration.status !== "planning" && workspace.collaboration.status !== "active") {
+ return false;
+ }
+ if (workspace.deliverables.length === 0) return true;
+ return workspace.deliverables.every((d) => d.status === "completed");
+ };
+
+ const handleLogoClick = async () => {
+ try {
+ const profile = await getUserProfile();
+ if (profile) {
+ if (profile.role === "Creator") {
+ router.push("/creator/home");
+ } else if (profile.role === "Brand") {
+ router.push("/brand/home");
+ }
+ }
+ } catch (err) {
+ console.error("Failed to get user profile:", err);
+ }
+ };
+
+ if (!collaborationId) {
+ return (
+
+
+
+
Invalid collaboration
+
+ We couldn't determine which collaboration workspace to load. Please return to your collaborations list
+ and try again.
+
+
router.push("/creator/collaborations/manage")}
+ className="mt-6 rounded-md bg-purple-600 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-purple-700"
+ >
+ Go to collaborations
+
+
+
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+
+
Loading workspace...
+
+
+
+ );
+ }
+
+ if (!workspace) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {/* Header */}
+
+
+
+
router.push("/creator/collaborations/manage")}
+ className="rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
+ >
+
+
+
+
+
+ InPactAI
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Collaboration Header */}
+
+
+
+
+
{workspace.collaboration.title}
+
+ {workspace.collaboration.status}
+
+
+
{workspace.collaboration.description || "No description provided."}
+
+
+
Collaborating with:
+
+ {workspace.other_creator.profile_picture_url ? (
+
+ ) : (
+
+ )}
+
{workspace.other_creator.display_name}
+
+
+
Type: {workspace.collaboration.collaboration_type}
+
+
+ {canComplete() && workspace.collaboration.status !== "completed" && (
+
+
+ Mark Complete
+
+ )}
+
+
+
+ {/* Tabs */}
+
+ {["overview", "deliverables", "messages", "assets"].map((tab) => (
+ setActiveTab(tab as any)}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors capitalize ${
+ activeTab === tab
+ ? "border-purple-600 text-purple-600"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ }`}
+ >
+ {tab}
+
+ ))}
+
+
+ {/* Tab Content */}
+ {activeTab === "overview" && (
+
+
+
+
Total Deliverables
+
{workspace.deliverables.length}
+
+
+
Completed
+
+ {workspace.deliverables.filter((d) => d.status === "completed").length}
+
+
+
+
Messages
+
{workspace.messages.length}
+
+
+
+ {workspace.collaboration.status === "completed" && (
+
+
Feedback
+ {workspace.feedback && workspace.feedback.length > 0 ? (
+
+ {workspace.feedback.map((fb) => {
+ const isMyFeedback = fb.from_creator_id === currentUserId;
+ const otherCreatorName = isMyFeedback
+ ? workspace.other_creator.display_name
+ : "You";
+ return (
+
+
+
+ {isMyFeedback ? "Your feedback to " : "Feedback from "}
+ {otherCreatorName}:
+
+ {fb.rating && (
+
+ {[1, 2, 3, 4, 5].map((star) => (
+
+ ))}
+
+ )}
+
+ {fb.feedback && (
+
{fb.feedback}
+ )}
+
+ );
+ })}
+
+ ) : (
+
+
No feedback submitted yet.
+
+ )}
+ {!showFeedback && workspace.feedback?.find((fb) => fb.from_creator_id === currentUserId) ? (
+
You have already submitted feedback.
+ ) : !showFeedback ? (
+
setShowFeedback(true)}
+ className="mt-4 rounded-md bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700"
+ >
+ Submit Feedback
+
+ ) : null}
+
+ {showFeedback && (
+
+
+
Rating (1-5)
+
+ {[1, 2, 3, 4, 5].map((star) => (
+ setFeedback({ ...feedback, rating: star })}
+ className="focus:outline-none"
+ >
+
+
+ ))}
+
+
+
+ Feedback
+ setFeedback({ ...feedback, feedback: e.target.value })}
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ rows={4}
+ placeholder="Share your thoughts about this collaboration..."
+ />
+
+
+
+ Submit
+
+ setShowFeedback(false)}
+ className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+
+
+ )}
+
+ )}
+
+ )}
+
+ {activeTab === "deliverables" && (
+
+
+
Deliverables
+ {workspace.collaboration.status !== "completed" && (
+
setShowAddDeliverable(true)}
+ className="flex items-center gap-2 rounded-md bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700"
+ >
+
+ Add Deliverable
+
+ )}
+
+
+ {showAddDeliverable && (
+
+ New Deliverable
+
+ Description *
+ setNewDeliverable({ ...newDeliverable, description: e.target.value })}
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ rows={4}
+ required
+ placeholder="Describe what needs to be delivered..."
+ />
+
+
+ Due Date
+ setNewDeliverable({ ...newDeliverable, due_date: e.target.value })}
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ />
+
+
+
+ Create
+
+ setShowAddDeliverable(false)}
+ className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+
+
+ )}
+
+ {workspace.deliverables.length === 0 ? (
+
+
+
No deliverables
+
Add deliverables to track your collaboration progress.
+
+ ) : (
+ workspace.deliverables.map((deliverable) => (
+
+
+
+
+
+ {deliverable.status}
+
+ {deliverable.due_date && (
+
+
+ Due: {formatDate(deliverable.due_date)}
+
+ )}
+
+
{deliverable.description}
+ {deliverable.submission_url && (
+
+ )}
+
+ {workspace.collaboration.status !== "completed" && (
+
+ {deliverable.status !== "completed" && (
+ handleUpdateDeliverableStatus(deliverable.id, "completed")}
+ className="flex items-center gap-2 rounded-md bg-green-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-green-700"
+ >
+
+ Mark Complete
+
+ )}
+
+ )}
+
+
+ ))
+ )}
+
+ )}
+
+ {activeTab === "messages" && (
+
+
+ {workspace.messages.length === 0 ? (
+
+
+
+
No messages yet. Start the conversation!
+
+
+ ) : (
+ workspace.messages.map((message) => (
+
+
+
{message.message}
+
+ {formatDateTime(message.created_at)}
+
+
+
+ ))
+ )}
+
+
+ {workspace.collaboration.status !== "completed" && workspace.collaboration.status !== "declined" && (
+
+
+ setMessageText(e.target.value)}
+ className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ placeholder="Type a message..."
+ />
+
+
+
+
+
+ )}
+
+ )}
+
+ {activeTab === "assets" && (
+
+
+
Shared Assets
+ {workspace.collaboration.status !== "completed" && (
+ setShowAddAsset(true)}
+ className="flex items-center gap-2 rounded-md bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700"
+ >
+
+ Add Asset
+
+ )}
+
+
+ {showAddAsset && (
+
+ Share Asset (URL)
+
+ URL *
+ setNewAsset({ ...newAsset, url: e.target.value })}
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ placeholder="https://..."
+ required
+ />
+
+
+ Type (Optional)
+ setNewAsset({ ...newAsset, type: e.target.value })}
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ placeholder="e.g., image, video, document"
+ />
+
+
+
+ Share
+
+ setShowAddAsset(false)}
+ className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+
+
+ )}
+
+ {workspace.assets.length === 0 ? (
+
+
+
No assets shared
+
Share files, links, or resources with your collaborator.
+
+ ) : (
+
+ {workspace.assets.map((asset) => (
+
+
+
{formatDate(asset.created_at)}
+
+ ))}
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
diff --git a/frontend/app/creator/collaborations/manage/page.tsx b/frontend/app/creator/collaborations/manage/page.tsx
new file mode 100644
index 0000000..31cad9d
--- /dev/null
+++ b/frontend/app/creator/collaborations/manage/page.tsx
@@ -0,0 +1,457 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import {
+ getMyCollaborations,
+ acceptCollaboration,
+ declineCollaboration,
+ type Collaboration,
+} from "@/lib/api/collaborations";
+import { getUserProfile } from "@/lib/auth-helpers";
+import { Check, X, Clock, Users, Sparkles, MessageSquare, FileText } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function ManageCollaborationsPage() {
+ const router = useRouter();
+ const [collaborations, setCollaborations] = useState([]);
+ const [incomingRequests, setIncomingRequests] = useState([]);
+ const [outgoingRequests, setOutgoingRequests] = useState([]);
+ const [proposedCollaborations, setProposedCollaborations] = useState([]);
+ const [activeCollaborations, setActiveCollaborations] = useState([]);
+ const [completedCollaborations, setCompletedCollaborations] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [activeTab, setActiveTab] = useState<"proposed" | "incoming" | "outgoing" | "active" | "completed">("proposed");
+ const [currentUserId, setCurrentUserId] = useState(null);
+
+ useEffect(() => {
+ loadCollaborations();
+ loadCurrentUser();
+ }, []);
+
+ const loadCurrentUser = async () => {
+ try {
+ const profile = await getUserProfile();
+ if (profile?.id) {
+ setCurrentUserId(profile.id);
+ }
+ } catch (err) {
+ console.error("Failed to load user profile:", err);
+ }
+ };
+
+ const loadCollaborations = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+ const data = await getMyCollaborations();
+ setCollaborations(data);
+
+ // Determine current creator ID from collaborations
+ // The current creator is either creator1_id or creator2_id in each collaboration
+ // We'll use the first collaboration to determine which one we are
+ let currentCreatorId: string | null = null;
+ if (data.length > 0) {
+ const firstCollab = data[0];
+ // We need to determine which creator we are
+ // We'll check if we're the initiator of any collaboration
+ // If we're the initiator, we know our creator ID
+ const initiatorCollab = data.find(c => c.initiator_id);
+ if (initiatorCollab && initiatorCollab.initiator_id) {
+ // Check if we're creator1 or creator2
+ if (initiatorCollab.creator1_id === initiatorCollab.initiator_id) {
+ currentCreatorId = initiatorCollab.creator1_id;
+ } else if (initiatorCollab.creator2_id === initiatorCollab.initiator_id) {
+ currentCreatorId = initiatorCollab.creator2_id;
+ }
+ }
+ // If we couldn't determine from initiator, use creator1_id from first collab
+ // (This is a fallback - in practice, we should get creator ID from backend)
+ if (!currentCreatorId) {
+ currentCreatorId = firstCollab.creator1_id;
+ }
+ }
+
+ setCurrentUserId(currentCreatorId);
+
+ // Filter collaborations based on initiator_id
+ const incoming = data.filter(
+ (collab) => collab.status === "proposed" && collab.initiator_id && collab.initiator_id !== currentCreatorId
+ );
+ const outgoing = data.filter(
+ (collab) => collab.status === "proposed" && collab.initiator_id === currentCreatorId
+ );
+ const proposed = data.filter((collab) => collab.status === "proposed");
+ const active = data.filter((collab) =>
+ ["accepted", "planning", "active"].includes(collab.status)
+ );
+ const completed = data.filter((collab) => collab.status === "completed");
+
+ setIncomingRequests(incoming);
+ setOutgoingRequests(outgoing);
+ setProposedCollaborations(proposed);
+ setActiveCollaborations(active);
+ setCompletedCollaborations(completed);
+ } catch (err: any) {
+ console.error("Failed to load collaborations:", err);
+ setError(err?.message || "Failed to load collaborations. Please try again.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleAccept = async (collaborationId: string) => {
+ try {
+ setError(null);
+ await acceptCollaboration(collaborationId);
+ await loadCollaborations();
+ } catch (err: any) {
+ console.error("Failed to accept collaboration:", err);
+ setError(err?.message || "Failed to accept collaboration. Please try again.");
+ }
+ };
+
+ const handleDecline = async (collaborationId: string) => {
+ if (!confirm("Are you sure you want to decline this collaboration?")) {
+ return;
+ }
+ try {
+ setError(null);
+ await declineCollaboration(collaborationId);
+ await loadCollaborations();
+ } catch (err: any) {
+ console.error("Failed to decline collaboration:", err);
+ setError(err?.message || "Failed to decline collaboration. Please try again.");
+ }
+ };
+
+ const handleOpenWorkspace = (collaborationId: string) => {
+ router.push(`/creator/collaborations/${collaborationId}`);
+ };
+
+ const getStatusColor = (status: string): string => {
+ const colors: Record = {
+ proposed: "bg-yellow-100 text-yellow-800",
+ accepted: "bg-blue-100 text-blue-800",
+ planning: "bg-purple-100 text-purple-800",
+ active: "bg-green-100 text-green-800",
+ completed: "bg-gray-100 text-gray-800",
+ declined: "bg-red-100 text-red-800",
+ cancelled: "bg-gray-100 text-gray-800",
+ };
+ return colors[status] || "bg-gray-100 text-gray-800";
+ };
+
+ const formatDate = (dateString: string | null): string => {
+ if (!dateString) return "N/A";
+ return new Date(dateString).toLocaleDateString();
+ };
+
+ const handleLogoClick = async () => {
+ try {
+ const profile = await getUserProfile();
+ if (profile) {
+ if (profile.role === "Creator") {
+ router.push("/creator/home");
+ } else if (profile.role === "Brand") {
+ router.push("/brand/home");
+ }
+ }
+ } catch (err) {
+ console.error("Failed to get user profile:", err);
+ }
+ };
+
+ const renderCollaborationCard = (collab: Collaboration, showActions: boolean = false) => {
+ const isInitiator = currentUserId === collab.initiator_id;
+
+ return (
+
+
+
+
+
{collab.title}
+
+ {collab.status}
+
+
+
{collab.description || "No description provided."}
+
+
+
+ Proposed: {formatDate(collab.proposed_at)}
+
+ {collab.accepted_at && (
+
+
+ Accepted: {formatDate(collab.accepted_at)}
+
+ )}
+ {collab.start_date && (
+ Start: {formatDate(collab.start_date)}
+ )}
+ {collab.end_date && (
+ End: {formatDate(collab.end_date)}
+ )}
+
+ {collab.proposal_message && (
+
+
+ Message:
+ {collab.proposal_message}
+
+
+ )}
+
+ Type:
+ {collab.collaboration_type}
+
+
+
+
+ {showActions && !isInitiator && collab.status === "proposed" && (
+ <>
+ handleAccept(collab.id)}
+ className="flex items-center gap-2 rounded-md bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700"
+ >
+
+ Accept
+
+ handleDecline(collab.id)}
+ className="flex items-center gap-2 rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
+ >
+
+ Decline
+
+ >
+ )}
+ {!showActions && (collab.status === "accepted" || collab.status === "planning" || collab.status === "active") && (
+ handleOpenWorkspace(collab.id)}
+ className="flex items-center gap-2 rounded-md bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700"
+ >
+
+ Open Workspace
+
+ )}
+ {collab.status === "completed" && (
+ handleOpenWorkspace(collab.id)}
+ className="flex items-center gap-2 rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700"
+ >
+
+ View Details
+
+ )}
+
+
+ );
+ };
+
+ return (
+
+
+
+
+ {/* Header */}
+
+
+
+
+
+ InPactAI
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Page Title */}
+
+
Manage Collaborations
+
View and manage your collaboration requests and active collaborations
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Tabs */}
+
+ setActiveTab("proposed")}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
+ activeTab === "proposed"
+ ? "border-purple-600 text-purple-600"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ }`}
+ >
+ Proposed ({proposedCollaborations.length})
+
+ setActiveTab("incoming")}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
+ activeTab === "incoming"
+ ? "border-purple-600 text-purple-600"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ }`}
+ >
+ Incoming ({incomingRequests.length})
+
+ setActiveTab("outgoing")}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
+ activeTab === "outgoing"
+ ? "border-purple-600 text-purple-600"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ }`}
+ >
+ Outgoing ({outgoingRequests.length})
+
+ setActiveTab("active")}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
+ activeTab === "active"
+ ? "border-purple-600 text-purple-600"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ }`}
+ >
+ Active ({activeCollaborations.length})
+
+ setActiveTab("completed")}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
+ activeTab === "completed"
+ ? "border-purple-600 text-purple-600"
+ : "border-transparent text-gray-500 hover:text-gray-700"
+ }`}
+ >
+ Completed ({completedCollaborations.length})
+
+
+
+ {/* Content */}
+ {isLoading ? (
+
+
+
+
Loading collaborations...
+
+
+ ) : (
+
+ {activeTab === "proposed" && (
+ <>
+ {proposedCollaborations.length === 0 ? (
+
+
+
No proposed collaborations
+
You don't have any pending collaboration proposals.
+
router.push("/creator/collaborations")}
+ className="mt-4 rounded-md bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700"
+ >
+ Find Creators to Collaborate
+
+
+ ) : (
+
+ {/* Incoming Section */}
+ {incomingRequests.length > 0 && (
+
+
Incoming Requests
+
+ {incomingRequests.map((collab) => renderCollaborationCard(collab, true))}
+
+
+ )}
+ {/* Outgoing Section */}
+ {outgoingRequests.length > 0 && (
+
+
Outgoing Requests
+
+ {outgoingRequests.map((collab) => renderCollaborationCard(collab, false))}
+
+
+ )}
+
+ )}
+ >
+ )}
+
+ {activeTab === "incoming" && (
+ <>
+ {incomingRequests.length === 0 ? (
+
+
+
No incoming requests
+
You don't have any pending collaboration requests.
+
+ ) : (
+ incomingRequests.map((collab) => renderCollaborationCard(collab, true))
+ )}
+ >
+ )}
+
+ {activeTab === "outgoing" && (
+ <>
+ {outgoingRequests.length === 0 ? (
+
+
+
No outgoing requests
+
You haven't sent any collaboration proposals yet.
+
+ ) : (
+ outgoingRequests.map((collab) => renderCollaborationCard(collab, false))
+ )}
+ >
+ )}
+
+ {activeTab === "active" && (
+ <>
+ {activeCollaborations.length === 0 ? (
+
+
+
No active collaborations
+
You don't have any active collaborations at the moment.
+
+ ) : (
+ activeCollaborations.map((collab) => renderCollaborationCard(collab, false))
+ )}
+ >
+ )}
+
+ {activeTab === "completed" && (
+ <>
+ {completedCollaborations.length === 0 ? (
+
+
+
No completed collaborations
+
You haven't completed any collaborations yet.
+
+ ) : (
+ completedCollaborations.map((collab) => renderCollaborationCard(collab, false))
+ )}
+ >
+ )}
+
+ )}
+
+
+
+ );
+}
+
diff --git a/frontend/app/creator/collaborations/page.tsx b/frontend/app/creator/collaborations/page.tsx
new file mode 100644
index 0000000..0aa4227
--- /dev/null
+++ b/frontend/app/creator/collaborations/page.tsx
@@ -0,0 +1,1145 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import { generateCollaborationIdeas, recommendCreatorForIdea, proposeCollaboration, type CollaborationIdea, type RecommendCreatorResponse } from "@/lib/api/collaborations";
+import { getCreatorDetails, getCreatorRecommendations, listCreators, type CreatorBasic, type CreatorFull, type CreatorRecommendation } from "@/lib/api/creators";
+import { getUserProfile } from "@/lib/auth-helpers";
+import { ChevronDown, ChevronUp, ExternalLink, Facebook, Globe, Instagram, Lightbulb, Linkedin, Search, Sparkles, Twitch, Twitter, Users, X, Youtube, Send } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function CollaborationsPage() {
+ const router = useRouter();
+ const [creators, setCreators] = useState([]);
+ const [recommended, setRecommended] = useState([]);
+ const [expandedCreator, setExpandedCreator] = useState(null);
+ const [expandedDetails, setExpandedDetails] = useState(null);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [isLoading, setIsLoading] = useState(true);
+ const [isLoadingRecommended, setIsLoadingRecommended] = useState(false);
+ const [isLoadingDetails, setIsLoadingDetails] = useState(false);
+ const [error, setError] = useState(null);
+ const [collaborationIdeas, setCollaborationIdeas] = useState([]);
+ const [isLoadingIdeas, setIsLoadingIdeas] = useState(false);
+ const [showIdeasPopup, setShowIdeasPopup] = useState(false);
+ const [activeTab, setActiveTab] = useState<"browse" | "ai">("browse");
+ const [collabIdeaInput, setCollabIdeaInput] = useState("");
+ const [isLoadingRecommendation, setIsLoadingRecommendation] = useState(false);
+ const [ideaRecommendation, setIdeaRecommendation] = useState(null);
+ const [showProposalModal, setShowProposalModal] = useState(false);
+ const [selectedIdea, setSelectedIdea] = useState(null);
+ const [selectedCreatorId, setSelectedCreatorId] = useState(null);
+ const [proposalData, setProposalData] = useState({
+ title: "",
+ description: "",
+ proposal_message: "",
+ start_date: "",
+ end_date: "",
+ });
+ const [isSubmittingProposal, setIsSubmittingProposal] = useState(false);
+
+ useEffect(() => {
+ loadCreators();
+ }, []);
+
+ useEffect(() => {
+ if (activeTab === "ai") {
+ loadRecommendations();
+ }
+ }, [activeTab]);
+
+ const loadCreators = async (search?: string) => {
+ try {
+ setIsLoading(true);
+ setError(null);
+ const data = await listCreators({ search, limit: 100 });
+ setCreators(data);
+ } catch (err: any) {
+ console.error("Failed to load creators:", err);
+ const errorMessage = err?.message || "Failed to load creators. Please try again.";
+
+ // Check if it's an authentication error
+ if (errorMessage.includes("Unauthorized") || errorMessage.includes("authentication token")) {
+ // Don't redirect automatically - let AuthGuard handle it
+ // This could be a backend configuration issue (JWT secret not set)
+ setError("Authentication failed. This might be a backend configuration issue. Please check if SUPABASE_JWT_SECRET is set in the backend .env file.");
+ } else if (errorMessage.includes("Creator profile not found")) {
+ setError("Please complete your creator onboarding first.");
+ } else {
+ setError(errorMessage);
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const loadRecommendations = async () => {
+ try {
+ setIsLoadingRecommended(true);
+ setError(null);
+ const data = await getCreatorRecommendations(4);
+ setRecommended(data);
+ } catch (err: any) {
+ console.error("Failed to load recommendations:", err);
+ setError(err?.message || "Failed to load recommendations. Please try again.");
+ } finally {
+ setIsLoadingRecommended(false);
+ }
+ };
+
+ const handleSearch = (e: React.FormEvent) => {
+ e.preventDefault();
+ loadCreators(searchQuery);
+ };
+
+ const handleExpand = async (creatorId: string) => {
+ if (expandedCreator === creatorId) {
+ // Collapse
+ setExpandedCreator(null);
+ setExpandedDetails(null);
+ setCollaborationIdeas([]);
+ } else {
+ // Expand
+ setExpandedCreator(creatorId);
+ setIsLoadingDetails(true);
+ setCollaborationIdeas([]);
+ try {
+ const details = await getCreatorDetails(creatorId);
+ setExpandedDetails(details);
+ } catch (err) {
+ console.error("Failed to load creator details:", err);
+ setError("Failed to load creator details.");
+ } finally {
+ setIsLoadingDetails(false);
+ }
+ }
+ };
+
+ const handleGenerateIdeas = async (targetCreatorId: string) => {
+ setIsLoadingIdeas(true);
+ setError(null);
+ try {
+ const response = await generateCollaborationIdeas(targetCreatorId);
+ setCollaborationIdeas(response.ideas);
+ } catch (err: any) {
+ console.error("Failed to generate collaboration ideas:", err);
+ setError(err?.message || "Failed to generate collaboration ideas. Please try again.");
+ } finally {
+ setIsLoadingIdeas(false);
+ }
+ };
+
+ const formatNumber = (num: number | null | undefined): string => {
+ if (!num) return "0";
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
+ if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
+ return num.toString();
+ };
+
+ const getSocialIcon = (platform: string) => {
+ const icons: Record = {
+ youtube: Youtube,
+ instagram: Instagram,
+ twitter: Twitter,
+ facebook: Facebook,
+ linkedin: Linkedin,
+ twitch: Twitch,
+ };
+ return icons[platform.toLowerCase()] || ExternalLink;
+ };
+
+ const getScoreColor = (score: number): string => {
+ if (score >= 70) return "bg-green-500";
+ if (score >= 40) return "bg-yellow-500";
+ return "bg-red-500";
+ };
+
+ const handleLogoClick = async () => {
+ try {
+ const profile = await getUserProfile();
+ if (profile) {
+ if (profile.role === "Creator") {
+ router.push("/creator/home");
+ } else if (profile.role === "Brand") {
+ router.push("/brand/home");
+ }
+ }
+ } catch (err) {
+ console.error("Failed to get user profile:", err);
+ }
+ };
+
+ const handleGetRecommendation = async () => {
+ if (!collabIdeaInput.trim() || recommended.length === 0) {
+ setError("Please enter a collaboration idea first.");
+ return;
+ }
+
+ setIsLoadingRecommendation(true);
+ setError(null);
+ setIdeaRecommendation(null);
+
+ try {
+ const candidateIds = recommended.map((r) => r.id);
+ const response = await recommendCreatorForIdea(collabIdeaInput, candidateIds);
+ setIdeaRecommendation(response);
+ } catch (err: any) {
+ console.error("Failed to get recommendation:", err);
+ setError(err?.message || "Failed to get recommendation. Please try again.");
+ } finally {
+ setIsLoadingRecommendation(false);
+ }
+ };
+
+ const handleProposeCollaboration = (idea: CollaborationIdea, creatorId: string) => {
+ setSelectedIdea(idea);
+ setSelectedCreatorId(creatorId);
+ setProposalData({
+ title: idea.title,
+ description: idea.description,
+ proposal_message: `Hi! I'd love to collaborate on "${idea.title}". ${idea.description}`,
+ start_date: "",
+ end_date: "",
+ });
+ setShowProposalModal(true);
+ };
+
+ const handleSubmitProposal = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!selectedCreatorId || !selectedIdea) return;
+
+ setIsSubmittingProposal(true);
+ setError(null);
+
+ try {
+ await proposeCollaboration({
+ target_creator_id: selectedCreatorId,
+ collaboration_type: selectedIdea.collaboration_type,
+ title: proposalData.title,
+ description: proposalData.description,
+ proposal_message: proposalData.proposal_message,
+ start_date: proposalData.start_date || undefined,
+ end_date: proposalData.end_date || undefined,
+ });
+ setShowProposalModal(false);
+ setSelectedIdea(null);
+ setSelectedCreatorId(null);
+ setProposalData({
+ title: "",
+ description: "",
+ proposal_message: "",
+ start_date: "",
+ end_date: "",
+ });
+ alert("Collaboration proposal sent successfully!");
+ } catch (err: any) {
+ console.error("Failed to propose collaboration:", err);
+ setError(err?.message || "Failed to send proposal. Please try again.");
+ } finally {
+ setIsSubmittingProposal(false);
+ }
+ };
+
+ return (
+
+
+
+
+ {/* Header */}
+
+
+
+
+
+ InPactAI
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Page Title */}
+
+
Find Creators
+
Discover and connect with other creators for collaborations
+
+
+ {/* Tabs */}
+
+ setActiveTab("browse")}
+ className={`rounded-md px-4 py-2 text-sm font-medium ${activeTab === "browse" ? "bg-purple-600 text-white" : "bg-white text-gray-700 border border-gray-300 hover:bg-gray-50"}`}
+ >
+ Browse
+
+ setActiveTab("ai")}
+ className={`rounded-md px-4 py-2 text-sm font-medium ${activeTab === "ai" ? "bg-purple-600 text-white" : "bg-white text-gray-700 border border-gray-300 hover:bg-gray-50"}`}
+ >
+ AI Matches
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {activeTab === "browse" && (
+ <>
+ {/* Search Bar */}
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Search by name, niche, or bio..."
+ className="w-full rounded-lg border border-gray-300 bg-white py-3 pl-12 pr-4 text-gray-900 placeholder-gray-500 focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ />
+
+ Search
+
+
+
+ {/* Loading State */}
+ {isLoading ? (
+
+
+
+
Loading creators...
+
+
+ ) : creators.length === 0 ? (
+
+
+
+
No creators found
+
+ {searchQuery ? "Try adjusting your search query." : "No creators available at the moment."}
+
+
+
+ ) : (
+ /* Creators Grid */
+
+ {creators.map((creator) => (
+
+ {/* Basic Card Content */}
+
+
+ {/* Profile Picture */}
+
+ {creator.profile_picture_url ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* Basic Info */}
+
+
+
+ {creator.display_name}
+
+ {creator.is_verified_creator && (
+
+ Verified
+
+ )}
+
+ {creator.tagline && (
+
{creator.tagline}
+ )}
+
+
+
+ {formatNumber(creator.total_followers)}
+
+ {creator.engagement_rate && (
+ {creator.engagement_rate.toFixed(1)}% engagement
+ )}
+
+
+
+ {creator.primary_niche}
+
+
+
+
+
+ {/* Expand/Collapse Button */}
+
handleExpand(creator.id)}
+ className="mt-4 flex w-full items-center justify-center gap-2 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ {expandedCreator === creator.id ? (
+ <>
+
+ Hide Details
+ >
+ ) : (
+ <>
+
+ View Details
+ >
+ )}
+
+
+
+ {/* Expanded Details */}
+ {expandedCreator === creator.id && (
+
+ {isLoadingDetails ? (
+
+ ) : expandedDetails ? (
+
+ {/* Bio */}
+ {expandedDetails.bio && (
+
+
About
+
{expandedDetails.bio}
+
+ )}
+
+ {/* Niches */}
+
+
Niches
+
+
+ {expandedDetails.primary_niche}
+
+ {expandedDetails.secondary_niches?.map((niche, idx) => (
+
+ {niche}
+
+ ))}
+
+
+
+ {/* Social Platforms */}
+
+
+ {/* Stats */}
+
+ {expandedDetails.total_reach && (
+
+
Total Reach
+
+ {formatNumber(expandedDetails.total_reach)}
+
+
+ )}
+ {expandedDetails.average_views && (
+
+
Avg Views
+
+ {formatNumber(expandedDetails.average_views)}
+
+
+ )}
+ {expandedDetails.posting_frequency && (
+
+
Posting Frequency
+
+ {expandedDetails.posting_frequency}
+
+
+ )}
+ {expandedDetails.years_of_experience && (
+
+
Experience
+
+ {expandedDetails.years_of_experience} years
+
+
+ )}
+
+
+ {/* Content Types */}
+ {expandedDetails.content_types && expandedDetails.content_types.length > 0 && (
+
+
Content Types
+
+ {expandedDetails.content_types.map((type, idx) => (
+
+ {type}
+
+ ))}
+
+
+ )}
+
+ {/* Collaboration Types */}
+ {expandedDetails.collaboration_types && expandedDetails.collaboration_types.length > 0 && (
+
+
Open to Collaborations
+
+ {expandedDetails.collaboration_types.map((type, idx) => (
+
+ {type}
+
+ ))}
+
+
+ )}
+
+ {/* Generate Collaboration Ideas Button */}
+
+
handleGenerateIdeas(expandedDetails.id)}
+ disabled={isLoadingIdeas}
+ className="flex w-full items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+ style={{ backgroundColor: "#FFC61A" }}
+ >
+ {isLoadingIdeas ? (
+ <>
+
+ Generating Ideas...
+ >
+ ) : (
+ <>
+
+ Generate Collaboration Ideas
+ >
+ )}
+
+
+
+ {/* Display First Idea */}
+ {collaborationIdeas.length > 0 && (
+
+
+
+
Collaboration Idea
+
+
+ {collaborationIdeas[0].title}
+
+
+ {collaborationIdeas[0].description}
+
+
+
+ {collaborationIdeas[0].collaboration_type}
+
+
+
+ {collaborationIdeas[0].why_it_works}
+
+
+ handleProposeCollaboration(collaborationIdeas[0], expandedDetails.id)}
+ className="flex-1 flex items-center justify-center gap-2 rounded-md bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700"
+ >
+
+ Propose Collaboration
+
+ {collaborationIdeas.length > 1 && (
+ setShowIdeasPopup(true)}
+ className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ More Ideas ({collaborationIdeas.length - 1})
+
+ )}
+
+
+ )}
+
+ ) : null}
+
+ )}
+
+ ))}
+
+ )}
+ >)}
+
+ {activeTab === "ai" && (
+
+ {/* Info text */}
+
+ Top collaborators selected for you using profile and audience compatibility. Click the yellow bulb to generate tailored collab ideas.
+
+
+ {/* Collaboration Idea Input */}
+ {recommended.length > 0 && (
+
+
+ Have a collaboration idea? Get AI recommendation for the best creator match:
+
+
+ setCollabIdeaInput(e.target.value)}
+ placeholder="E.g., I want to create a fitness challenge series targeting millennials..."
+ className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ rows={3}
+ />
+
+ {isLoadingRecommendation ? "Analyzing..." : "Get Recommendation"}
+
+
+ {ideaRecommendation && (
+
+
+
+
Best Match
+
+
+ {ideaRecommendation.recommended_creator.profile_picture_url ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {ideaRecommendation.recommended_creator.display_name}
+
+
+ {ideaRecommendation.recommended_creator.reasoning}
+
+
+
Match Score:
+
+ {ideaRecommendation.recommended_creator.match_score.toFixed(0)}
+
+
+
+
+
+ {ideaRecommendation.alternatives.length > 0 && (
+
+
Other options:
+
+ {ideaRecommendation.alternatives.map((alt) => (
+
+ {alt.display_name} - {alt.reasoning.substring(0, 100)}...
+
+ ))}
+
+
+ )}
+
+ )}
+
+ )}
+ {/* Loading State */}
+ {isLoadingRecommended ? (
+
+
+
+
Loading AI matches...
+
+
+ ) : recommended.length === 0 ? (
+
+ No AI matches found right now. Try updating your profile or expanding your niches.
+
+ ) : (
+
+ {recommended.map((rec) => (
+
+
+
+ {rec.profile_picture_url ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
{rec.display_name}
+
+
{rec.match_score.toFixed(0)}
+
+
+
+ {rec.primary_niche && (
+
+
+ {rec.primary_niche}
+
+
+ )}
+
+
+
+ {formatNumber(rec.total_followers || 0)}
+
+ {rec.engagement_rate != null && (
+ {rec.engagement_rate.toFixed(1)}% engagement
+ )}
+ {rec.top_platforms && rec.top_platforms.length > 0 && (
+ {rec.top_platforms.join(" · ")}
+ )}
+
+
+ {rec.reason}
+
+
+ handleGenerateIdeas(rec.id)}
+ disabled={isLoadingIdeas}
+ className="flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
+ style={{ backgroundColor: "#FFC61A" }}
+ >
+
+ {isLoadingIdeas ? "Generating..." : "Generate Ideas"}
+
+ handleExpand(rec.id)}
+ className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ {expandedCreator === rec.id ? (
+ <>
+
+ Hide
+ >
+ ) : (
+ <>
+
+ View Details
+ >
+ )}
+
+
+
+
+ {/* Expanded Details for AI Matches */}
+ {expandedCreator === rec.id && (
+
+ {isLoadingDetails ? (
+
+ ) : expandedDetails ? (
+
+ {expandedDetails.bio && (
+
+
About
+
{expandedDetails.bio}
+
+ )}
+
+
Niches
+
+
+ {expandedDetails.primary_niche}
+
+ {expandedDetails.secondary_niches?.map((niche, idx) => (
+
+ {niche}
+
+ ))}
+
+
+ {expandedDetails.content_types && expandedDetails.content_types.length > 0 && (
+
+
Content Types
+
+ {expandedDetails.content_types.map((type, idx) => (
+
+ {type}
+
+ ))}
+
+
+ )}
+ {/* Generate Collaboration Ideas Button */}
+
+
handleGenerateIdeas(expandedDetails.id)}
+ disabled={isLoadingIdeas}
+ className="flex w-full items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+ style={{ backgroundColor: "#FFC61A" }}
+ >
+ {isLoadingIdeas ? (
+ <>
+
+ Generating Ideas...
+ >
+ ) : (
+ <>
+
+ Generate Collaboration Ideas
+ >
+ )}
+
+
+ {/* Display First Idea */}
+ {collaborationIdeas.length > 0 && (
+
+
+
+
Collaboration Idea
+
+
+ {collaborationIdeas[0].title}
+
+
+ {collaborationIdeas[0].description}
+
+
+
+ {collaborationIdeas[0].collaboration_type}
+
+
+
+ {collaborationIdeas[0].why_it_works}
+
+
+ handleProposeCollaboration(collaborationIdeas[0], expandedDetails.id)}
+ className="flex-1 flex items-center justify-center gap-2 rounded-md bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700"
+ >
+
+ Propose Collaboration
+
+ {collaborationIdeas.length > 1 && (
+ setShowIdeasPopup(true)}
+ className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ More Ideas ({collaborationIdeas.length - 1})
+
+ )}
+
+
+ )}
+
+ ) : null}
+
+ )}
+
+ ))}
+
+ )}
+
+ )}
+
+
+ {/* Collaboration Ideas Popup Modal */}
+ {showIdeasPopup && collaborationIdeas.length > 0 && (
+
+
+ {/* Header */}
+
+
+
+
Collaboration Ideas
+
+
setShowIdeasPopup(false)}
+ className="rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
+ >
+
+
+
+
+ {/* Ideas List */}
+
+
+ {collaborationIdeas.map((idea, idx) => (
+
+
+
+ {idx + 1}
+
+
{idea.title}
+
+
{idea.description}
+
+
+ {idea.collaboration_type}
+
+
+
+
Why it works:
+
{idea.why_it_works}
+
+
{
+ const creatorId = expandedCreator || recommended.find(r => r.id === expandedCreator)?.id;
+ if (creatorId) {
+ handleProposeCollaboration(idea, creatorId);
+ setShowIdeasPopup(false);
+ }
+ }}
+ className="mt-4 w-full flex items-center justify-center gap-2 rounded-md bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700"
+ >
+
+ Propose This Collaboration
+
+
+ ))}
+
+
+
+ {/* Footer */}
+
+ setShowIdeasPopup(false)}
+ className="w-full rounded-md px-4 py-2 text-sm font-medium text-white transition-colors"
+ style={{ backgroundColor: "#FFC61A" }}
+ >
+ Close
+
+
+
+
+ )}
+
+ {/* Proposal Modal */}
+ {showProposalModal && selectedIdea && (
+
+
+ {/* Header */}
+
+
+
+
Propose Collaboration
+
+
setShowProposalModal(false)}
+ className="rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
+ >
+
+
+
+
+ {/* Form */}
+
+
+ Title *
+ setProposalData({ ...proposalData, title: e.target.value })}
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ required
+ />
+
+
+
+ Description
+ setProposalData({ ...proposalData, description: e.target.value })}
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ rows={4}
+ />
+
+
+
+ Message to Creator *
+ setProposalData({ ...proposalData, proposal_message: e.target.value })}
+ className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
+ rows={4}
+ required
+ placeholder="Introduce yourself and explain why you'd like to collaborate..."
+ />
+
+
+
+
+
+
+ {isSubmittingProposal ? "Sending..." : "Send Proposal"}
+
+ setShowProposalModal(false)}
+ className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/app/creator/contracts/page.tsx b/frontend/app/creator/contracts/page.tsx
new file mode 100644
index 0000000..81158df
--- /dev/null
+++ b/frontend/app/creator/contracts/page.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import { ContractsWorkspace } from "@/components/contracts/ContractsWorkspace";
+import SlidingMenu from "@/components/SlidingMenu";
+
+export default function CreatorContractsPage() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/frontend/app/creator/createcampaign/page.tsx b/frontend/app/creator/createcampaign/page.tsx
new file mode 100644
index 0000000..9b141d1
--- /dev/null
+++ b/frontend/app/creator/createcampaign/page.tsx
@@ -0,0 +1,10 @@
+export default function CreatorCreateCampaign() {
+ return (
+
+ Create Campaign
+
+ Welcome to Create Campaign — coming soon!
+
+
+ );
+}
diff --git a/frontend/app/creator/home/page.tsx b/frontend/app/creator/home/page.tsx
new file mode 100644
index 0000000..4bb61ea
--- /dev/null
+++ b/frontend/app/creator/home/page.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import SlidingMenu from "@/components/SlidingMenu";
+import CreatorDashboard from "@/components/dashboard/CreatorDashboard";
+import ProfileButton from "@/components/profile/ProfileButton";
+import { getUserProfile, signOut } from "@/lib/auth-helpers";
+import { Loader2, LogOut, Sparkles } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function CreatorHomePage() {
+ const router = useRouter();
+ const [userName, setUserName] = useState("");
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+
+ useEffect(() => {
+ async function loadProfile() {
+ const profile = await getUserProfile();
+ if (profile) {
+ setUserName(profile.name);
+ }
+ }
+ loadProfile();
+ }, []);
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+ await signOut();
+ router.push("/login");
+ } catch (error) {
+ console.error("Logout error:", error);
+ setIsLoggingOut(false);
+ }
+ };
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+ InPactAI
+
+
+
+
+
+ {isLoggingOut ? (
+
+ ) : (
+
+ )}
+ {isLoggingOut ? "Logging out..." : "Logout"}
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Welcome Header */}
+
+
+ Welcome back, {userName || "Creator"}!
+
+
+ Here's an overview of your campaigns, earnings, and performance
+
+
+
+ {/* Dashboard */}
+
+
+
+
+ );
+}
diff --git a/frontend/app/creator/onboarding/page.tsx b/frontend/app/creator/onboarding/page.tsx
new file mode 100644
index 0000000..4dfce71
--- /dev/null
+++ b/frontend/app/creator/onboarding/page.tsx
@@ -0,0 +1,752 @@
+"use client";
+
+import ImageUpload from "@/components/onboarding/ImageUpload";
+import MultiSelect from "@/components/onboarding/MultiSelect";
+import ProgressBar from "@/components/onboarding/ProgressBar";
+import TypeformQuestion from "@/components/onboarding/TypeformQuestion";
+import { uploadProfilePicture } from "@/lib/storage-helpers";
+import { supabase } from "@/lib/supabaseClient";
+import { AnimatePresence } from "framer-motion";
+import { CheckCircle2, Loader2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+const TOTAL_STEPS = 10;
+
+// Niche options
+const NICHE_OPTIONS = [
+ "Gaming",
+ "Technology",
+ "Fashion & Beauty",
+ "Fitness & Health",
+ "Food & Cooking",
+ "Travel & Lifestyle",
+ "Education & Tutorial",
+ "Entertainment & Comedy",
+ "Business & Finance",
+ "Arts & Crafts",
+ "Music",
+ "Sports",
+ "Parenting & Family",
+ "Home & Garden",
+ "Automotive",
+ "Other",
+];
+
+// Collaboration types
+const COLLABORATION_TYPES = [
+ "Sponsored Posts",
+ "Product Reviews",
+ "Brand Ambassadorships",
+ "Affiliate Marketing",
+ "Event Appearances",
+ "Content Co-creation",
+];
+
+// Social platforms
+const SOCIAL_PLATFORMS = [
+ "YouTube",
+ "Instagram",
+ "TikTok",
+ "Twitter",
+ "Twitch",
+ "Facebook",
+ "LinkedIn",
+ "Snapchat",
+];
+
+// Content types
+const CONTENT_TYPES = [
+ "Videos",
+ "Shorts",
+ "Reels",
+ "Stories",
+ "Posts",
+ "Blogs",
+ "Podcasts",
+ "Live Streams",
+];
+
+// Languages
+const LANGUAGES = [
+ "English",
+ "Hindi",
+ "Spanish",
+ "French",
+ "German",
+ "Portuguese",
+ "Japanese",
+ "Korean",
+ "Chinese",
+ "Arabic",
+ "Other",
+];
+
+// Posting frequency
+const POSTING_FREQUENCIES = [
+ "Daily",
+ "3x per week",
+ "Weekly",
+ "Bi-weekly",
+ "Monthly",
+];
+
+// Follower ranges
+const FOLLOWER_RANGES = [
+ "Less than 1K",
+ "1K - 10K",
+ "10K - 50K",
+ "50K - 100K",
+ "100K - 500K",
+ "500K - 1M",
+ "1M+",
+];
+
+interface SocialPlatform {
+ platform: string;
+ handle: string;
+ followers: string;
+}
+
+interface CreatorFormData {
+ displayName: string;
+ tagline: string;
+ bio: string;
+ primaryNiche: string;
+ secondaryNiches: string[];
+ socialPlatforms: SocialPlatform[];
+ contentTypes: string[];
+ postingFrequency: string;
+ contentLanguage: string[];
+ collaborationTypes: string[];
+ profilePicture: File | null;
+}
+
+export default function CreatorOnboardingPage() {
+ const router = useRouter();
+ const [currentStep, setCurrentStep] = useState(1);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [userId, setUserId] = useState(null);
+
+ const [formData, setFormData] = useState({
+ displayName: "",
+ tagline: "",
+ bio: "",
+ primaryNiche: "",
+ secondaryNiches: [],
+ socialPlatforms: [],
+ contentTypes: [],
+ postingFrequency: "",
+ contentLanguage: [],
+ collaborationTypes: [],
+ profilePicture: null,
+ });
+
+ // Temporary state for social platform input
+ const [newPlatform, setNewPlatform] = useState({
+ platform: "",
+ handle: "",
+ followers: "",
+ });
+
+ // Get user ID on mount
+ useEffect(() => {
+ const getUser = async () => {
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+ if (!user) {
+ router.push("/login");
+ return;
+ }
+ setUserId(user.id);
+ };
+ getUser();
+ }, [router]);
+
+ const handleNext = () => {
+ if (currentStep < TOTAL_STEPS) {
+ setCurrentStep(currentStep + 1);
+ }
+ };
+
+ const handleBack = () => {
+ if (currentStep > 1) {
+ setCurrentStep(currentStep - 1);
+ }
+ };
+
+ const updateFormData = (field: keyof CreatorFormData, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const addSocialPlatform = () => {
+ if (newPlatform.platform && newPlatform.handle && newPlatform.followers) {
+ setFormData((prev) => ({
+ ...prev,
+ socialPlatforms: [...prev.socialPlatforms, newPlatform],
+ }));
+ setNewPlatform({ platform: "", handle: "", followers: "" });
+ }
+ };
+
+ const removeSocialPlatform = (index: number) => {
+ setFormData((prev) => ({
+ ...prev,
+ socialPlatforms: prev.socialPlatforms.filter((_, i) => i !== index),
+ }));
+ };
+
+ const handleComplete = async () => {
+ if (!userId) return;
+
+ setIsSubmitting(true);
+
+ try {
+ // Upload profile picture if provided
+ let profilePictureUrl = null;
+ if (formData.profilePicture) {
+ profilePictureUrl = await uploadProfilePicture(
+ formData.profilePicture,
+ userId
+ );
+ }
+
+ // Insert creator profile
+ const { error: creatorError } = await supabase.from("creators").insert({
+ user_id: userId,
+ display_name: formData.displayName,
+ tagline: formData.tagline,
+ bio: formData.bio,
+ profile_picture_url: profilePictureUrl,
+ primary_niche: formData.primaryNiche,
+ secondary_niches: formData.secondaryNiches,
+ social_platforms: formData.socialPlatforms,
+ content_types: formData.contentTypes,
+ posting_frequency: formData.postingFrequency,
+ content_language: formData.contentLanguage,
+ collaboration_types: formData.collaborationTypes,
+ });
+
+ if (creatorError) throw creatorError;
+
+ // Mark onboarding as complete
+ const { error: profileError } = await supabase
+ .from("profiles")
+ .update({ onboarding_completed: true })
+ .eq("id", userId);
+
+ if (profileError) {
+ // Rollback: delete the creator profile
+ const { error: deleteError } = await supabase
+ .from("creators")
+ .delete()
+ .eq("user_id", userId);
+ if (deleteError) {
+ setIsSubmitting(false);
+ throw new Error(
+ `Onboarding failed and rollback also failed: ${profileError.message}; Cleanup error: ${deleteError.message}`
+ );
+ }
+ setIsSubmitting(false);
+ throw profileError;
+ }
+
+ // Show success and redirect
+ setTimeout(() => {
+ router.push("/creator/home");
+ }, 2000);
+ } catch (error: any) {
+ console.error("Onboarding error:", error);
+ alert("Failed to complete onboarding. Please try again.");
+ setIsSubmitting(false);
+ }
+ };
+
+ // Validation for each step
+ const canGoNext = () => {
+ switch (currentStep) {
+ case 1:
+ return true; // Welcome screen
+ case 2:
+ return formData.displayName.trim().length >= 2;
+ case 3:
+ return formData.bio.trim().length >= 50;
+ case 4:
+ return formData.primaryNiche !== "";
+ case 5:
+ return true; // Optional step
+ case 6:
+ return formData.socialPlatforms.length > 0;
+ case 7:
+ return (
+ formData.contentTypes.length > 0 &&
+ formData.postingFrequency !== "" &&
+ formData.contentLanguage.length > 0
+ );
+ case 8:
+ return formData.collaborationTypes.length > 0;
+ case 9:
+ return true; // Profile picture is optional
+ case 10:
+ return true; // Review step
+ default:
+ return false;
+ }
+ };
+
+ if (!userId) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+ {/* Step 1: Welcome */}
+ {currentStep === 1 && (
+
+
+
+ We'll ask you a few questions to build your profile and help
+ brands discover you. Ready?
+
+
+
+ )}
+
+ {/* Step 2: Display Name */}
+ {currentStep === 2 && (
+
+ updateFormData("displayName", e.target.value)}
+ placeholder="Your creator name"
+ className="w-full rounded-lg border-2 border-gray-300 px-6 py-4 text-lg transition-colors focus:border-purple-500 focus:outline-none"
+ autoFocus
+ />
+ {formData.displayName.length > 0 &&
+ formData.displayName.length < 2 && (
+
+ Display name must be at least 2 characters
+
+ )}
+
+ )}
+
+ {/* Step 3: Bio & Tagline */}
+ {currentStep === 3 && (
+
+
+
+
+ Tagline (Optional)
+
+ updateFormData("tagline", e.target.value)}
+ placeholder="A catchy one-liner about you"
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 transition-colors focus:border-purple-500 focus:outline-none"
+ />
+
+
+
+ Bio
+
+
updateFormData("bio", e.target.value)}
+ placeholder="Tell us your story, what you create, and what makes you unique..."
+ rows={5}
+ maxLength={500}
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 transition-colors focus:border-purple-500 focus:outline-none"
+ />
+
+ {formData.bio.length}/500 characters
+ {formData.bio.length < 50 && ` (minimum 50 characters)`}
+
+
+
+
+ )}
+
+ {/* Step 4: Primary Niche */}
+ {currentStep === 4 && (
+
+ updateFormData("primaryNiche", e.target.value)}
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 text-lg transition-colors focus:border-purple-500 focus:outline-none"
+ >
+ Select your primary niche
+ {NICHE_OPTIONS.map((niche) => (
+
+ {niche}
+
+ ))}
+
+
+ )}
+
+ {/* Step 5: Secondary Niches */}
+ {currentStep === 5 && (
+
+ n !== formData.primaryNiche)}
+ selected={formData.secondaryNiches}
+ onChange={(selected) =>
+ updateFormData("secondaryNiches", selected)
+ }
+ placeholder="Select additional niches"
+ />
+
+ )}
+
+ {/* Step 6: Social Media */}
+ {currentStep === 6 && (
+
+
+ {/* Existing platforms */}
+ {formData.socialPlatforms.map((platform, index) => (
+
+
+
+ {platform.platform}
+
+
+ @{platform.handle} • {platform.followers}
+
+
+
removeSocialPlatform(index)}
+ className="text-sm font-medium text-red-600 hover:text-red-700"
+ >
+ Remove
+
+
+ ))}
+
+ {/* Add new platform */}
+
+
+ Add Social Platform
+
+
+
+ setNewPlatform({
+ ...newPlatform,
+ platform: e.target.value,
+ })
+ }
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-2 focus:border-purple-500 focus:outline-none"
+ >
+ Select platform
+ {SOCIAL_PLATFORMS.map((platform) => (
+
+ {platform}
+
+ ))}
+
+
+ setNewPlatform({ ...newPlatform, handle: e.target.value })
+ }
+ placeholder="Username/handle"
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-2 focus:border-purple-500 focus:outline-none"
+ />
+
+ setNewPlatform({
+ ...newPlatform,
+ followers: e.target.value,
+ })
+ }
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-2 focus:border-purple-500 focus:outline-none"
+ >
+ Select follower count
+ {FOLLOWER_RANGES.map((range) => (
+
+ {range}
+
+ ))}
+
+
+ Add Platform
+
+
+
+
+
+ )}
+
+ {/* Step 7: Content Details */}
+ {currentStep === 7 && (
+
+
+
+ updateFormData("contentTypes", selected)
+ }
+ label="Content Types"
+ placeholder="Select at least one"
+ minSelection={1}
+ />
+
+
+
+ Posting Frequency
+
+
+ updateFormData("postingFrequency", e.target.value)
+ }
+ className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 focus:border-purple-500 focus:outline-none"
+ >
+ Select frequency
+ {POSTING_FREQUENCIES.map((freq) => (
+
+ {freq}
+
+ ))}
+
+
+
+
+ updateFormData("contentLanguage", selected)
+ }
+ label="Content Languages"
+ placeholder="Select at least one"
+ minSelection={1}
+ />
+
+
+ )}
+
+ {/* Step 8: Collaboration Preferences */}
+ {currentStep === 8 && (
+
+
+ updateFormData("collaborationTypes", selected)
+ }
+ placeholder="Select at least one collaboration type"
+ minSelection={1}
+ />
+
+ )}
+
+ {/* Step 9: Profile Picture */}
+ {currentStep === 9 && (
+
+ updateFormData("profilePicture", file)}
+ currentImage={formData.profilePicture}
+ label="Profile Picture"
+ maxSizeMB={5}
+ />
+
+ )}
+
+ {/* Step 10: Review & Submit */}
+ {currentStep === 10 && (
+
+ {isSubmitting ? (
+
+
+
+ Setting up your creator profile...
+
+
+ ) : (
+
+
+
+ Display Name
+
+
+ {formData.displayName}
+
+
+ {formData.tagline && (
+
+
Tagline
+
{formData.tagline}
+
+ )}
+
+
+
+ Primary Niche
+
+
{formData.primaryNiche}
+
+ {formData.secondaryNiches.length > 0 && (
+
+
+ Other Niches
+
+
+ {formData.secondaryNiches.join(", ")}
+
+
+ )}
+
+
+ Social Platforms ({formData.socialPlatforms.length})
+
+
+ {formData.socialPlatforms.map((platform, idx) => (
+
+ {platform.platform}: @{platform.handle} (
+ {platform.followers})
+
+ ))}
+
+
+
+
+ Content Info
+
+
+ {formData.contentTypes.join(", ")} •{" "}
+ {formData.postingFrequency} •{" "}
+ {formData.contentLanguage.join(", ")}
+
+
+
+
+ Collaboration Interests
+
+
+ {formData.collaborationTypes.join(", ")}
+
+
+ {formData.profilePicture && (
+
+
+ Profile picture added
+
+ )}
+
+ )}
+
+ )}
+
+ >
+ );
+}
diff --git a/frontend/app/creator/profile/page.tsx b/frontend/app/creator/profile/page.tsx
new file mode 100644
index 0000000..adab22f
--- /dev/null
+++ b/frontend/app/creator/profile/page.tsx
@@ -0,0 +1,1284 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import ArrayInput from "@/components/profile/ArrayInput";
+import CollapsibleSection from "@/components/profile/CollapsibleSection";
+import JsonInput from "@/components/profile/JsonInput";
+import ProfileButton from "@/components/profile/ProfileButton";
+import SlidingMenu from "@/components/SlidingMenu";
+import {
+ aiFillCreatorProfile,
+ CreatorProfile,
+ getCreatorProfile,
+ updateCreatorProfile,
+} from "@/lib/api/profile";
+import { signOut } from "@/lib/auth-helpers";
+import {
+ ArrowLeft,
+ Edit2,
+ Loader2,
+ LogOut,
+ Save,
+ Sparkles,
+ X,
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function CreatorProfilePage() {
+ const router = useRouter();
+ const [profile, setProfile] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [formData, setFormData] = useState>({});
+ const [aiLoading, setAiLoading] = useState(false);
+ const [aiInput, setAiInput] = useState("");
+ const [showAiModal, setShowAiModal] = useState(false);
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+
+ useEffect(() => {
+ loadProfile();
+ }, []);
+
+ const loadProfile = async () => {
+ try {
+ setLoading(true);
+ const data = await getCreatorProfile();
+ setProfile(data);
+ setFormData(data);
+ } catch (error) {
+ console.error("Error loading profile:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSave = async () => {
+ try {
+ setSaving(true);
+ const updated = await updateCreatorProfile(formData);
+ setProfile(updated);
+ setFormData(updated);
+ setIsEditing(false);
+ } catch (error) {
+ console.error("Error saving profile:", error);
+ alert("Failed to save profile. Please try again.");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (profile) {
+ setFormData(profile);
+ setIsEditing(false);
+ }
+ };
+
+ const handleAiFill = async () => {
+ if (!aiInput.trim()) {
+ alert("Please provide some information about yourself");
+ return;
+ }
+
+ try {
+ setAiLoading(true);
+ const result = await aiFillCreatorProfile(aiInput);
+
+ if (!result.data || Object.keys(result.data).length === 0) {
+ alert(
+ result.message ||
+ "No new data could be extracted from your input. Please provide more specific information."
+ );
+ return;
+ }
+
+ // Merge AI-generated data into form, handling all data types properly
+ setFormData((prev) => {
+ const updated = { ...prev };
+ for (const [key, value] of Object.entries(result.data)) {
+ // Properly handle arrays, objects, and primitives
+ if (Array.isArray(value)) {
+ updated[key] = value;
+ } else if (typeof value === "object" && value !== null) {
+ updated[key] = value;
+ } else {
+ updated[key] = value;
+ }
+ }
+ return updated;
+ });
+
+ setAiInput("");
+ setShowAiModal(false);
+
+ // Auto-enable edit mode if not already
+ if (!isEditing) {
+ setIsEditing(true);
+ }
+
+ // Show success message
+ const fieldCount = Object.keys(result.data).length;
+ alert(
+ `Success! ${fieldCount} field${fieldCount !== 1 ? "s" : ""} ${fieldCount !== 1 ? "were" : "was"} filled. Please review and save your changes.`
+ );
+ } catch (error: any) {
+ console.error("Error with AI fill:", error);
+ const errorMessage =
+ error?.message || "Failed to generate profile data. Please try again.";
+ alert(errorMessage);
+ } finally {
+ setAiLoading(false);
+ }
+ };
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+ await signOut();
+ router.push("/login");
+ } catch (error) {
+ console.error("Logout error:", error);
+ setIsLoggingOut(false);
+ }
+ };
+
+ const updateField = (field: string, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const updateArrayField = (field: string, values: string[]) => {
+ setFormData((prev) => ({ ...prev, [field]: values }));
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (!profile) {
+ return (
+
+
+
Failed to load profile
+
+
+ );
+ }
+
+ const completionPercentage = profile.profile_completion_percentage || 0;
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+ InPactAI
+
+
+
+
+
+ {isLoggingOut ? (
+
+ ) : (
+
+ )}
+ {isLoggingOut ? "Logging out..." : "Logout"}
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Back Button */}
+ router.push("/creator/home")}
+ className="mb-6 flex items-center gap-2 text-gray-600 hover:text-gray-900"
+ >
+
+ Back to Home
+
+
+ {/* Profile Header */}
+
+
+
+ Creator Profile
+
+
+ {!isEditing ? (
+ <>
+ setShowAiModal(true)}
+ className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-purple-600 to-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:shadow-lg"
+ >
+
+ AI Fill
+
+ setIsEditing(true)}
+ className="flex items-center gap-2 rounded-lg bg-purple-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-purple-700"
+ >
+
+ Edit
+
+ >
+ ) : (
+ <>
+
+
+ Cancel
+
+
+ {saving ? (
+
+ ) : (
+
+ )}
+ {saving ? "Saving..." : "Save"}
+
+ >
+ )}
+
+
+
+ {/* Completion Bar */}
+
+
+
+ Profile Completion
+
+
+ {completionPercentage}%
+
+
+
+
+
+
+ {/* Profile Form */}
+
+ {/* Basic Information */}
+
+
+
+
+ Display Name *
+
+
+ updateField("display_name", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Primary Niche *
+
+
+ updateField("primary_niche", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Tagline
+
+ updateField("tagline", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Website URL
+
+ updateField("website_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Bio
+
+ updateField("bio", e.target.value)}
+ disabled={!isEditing}
+ rows={4}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Profile Picture URL
+
+
+ updateField("profile_picture_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Cover Image URL
+
+
+ updateField("cover_image_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateArrayField("secondary_niches", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("content_types", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("content_language", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Social Media */}
+
+
+
+
+ Instagram Handle
+
+
+ updateField("instagram_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="@username"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Instagram URL
+
+
+ updateField("instagram_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Instagram Followers
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "instagram_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ YouTube Handle
+
+
+ updateField("youtube_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ YouTube URL
+
+ updateField("youtube_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ YouTube Subscribers
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "youtube_subscribers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ TikTok Handle
+
+
+ updateField("tiktok_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ TikTok URL
+
+ updateField("tiktok_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ TikTok Followers
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "tiktok_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Twitter Handle
+
+
+ updateField("twitter_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Twitter URL
+
+ updateField("twitter_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Twitter Followers
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "twitter_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Twitch Handle
+
+
+ updateField("twitch_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Twitch URL
+
+ updateField("twitch_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Twitch Followers
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "twitch_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ LinkedIn URL
+
+
+ updateField("linkedin_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Facebook URL
+
+
+ updateField("facebook_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Total Followers
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "total_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+ updateField("social_platforms", value)}
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Audience & Analytics */}
+
+
+
+
+ Total Reach
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "total_reach",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Average Views
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "average_views",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Engagement Rate (%)
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "engagement_rate",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Average Engagement per Post
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "average_engagement_per_post",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Primary Audience Age
+
+
+ updateField("audience_age_primary", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateArrayField("audience_age_secondary", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateField("audience_gender_split", value)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateField("audience_locations", value)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("audience_interests", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ Best Performing Content Type
+
+
+ updateField(
+ "best_performing_content_type",
+ e.target.value
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("peak_posting_times", value)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Content & Rates */}
+
+
+
+
+ Years of Experience
+
+
+ updateField(
+ "years_of_experience",
+ parseInt(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Posting Frequency
+
+
+ updateField("posting_frequency", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., Daily, 3x/week"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Rate per Post
+
+
+ updateField(
+ "rate_per_post",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Rate per Video
+
+
+ updateField(
+ "rate_per_video",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Rate per Story
+
+
+ updateField(
+ "rate_per_story",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Rate per Reel
+
+
+ updateField(
+ "rate_per_reel",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("rate_negotiable", e.target.checked)
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+ Rate Negotiable
+
+
+
+
+
+ Minimum Deal Value
+
+
+ updateField(
+ "minimum_deal_value",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Preferred Payment Terms
+
+
+ updateField("preferred_payment_terms", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField(
+ "accepts_product_only_deals",
+ e.target.checked
+ )
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+ Accepts Product-Only Deals
+
+
+
+
+
+
+ {/* Professional Details */}
+
+
+
+
+
+ updateField(
+ "content_creation_full_time",
+ e.target.checked
+ )
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+ Content Creation Full-Time
+
+
+
+
+
+ Team Size
+
+
+ updateField("team_size", parseInt(e.target.value) || 1)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ Equipment Quality
+
+
+ updateField("equipment_quality", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., Professional, Semi-Professional"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateArrayField("editing_software", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("collaboration_types", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_brands_style", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("not_interested_in", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Portfolio & Links */}
+
+
+
+
+ updateArrayField("portfolio_links", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("past_brand_collaborations", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("case_study_links", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ Media Kit URL
+
+
+ updateField("media_kit_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ {/* Additional Settings */}
+
+
+
+
+ updateArrayField("search_keywords", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ Matching Score Base
+
+
+ updateField(
+ "matching_score_base",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ AI Profile Summary
+
+
+ updateField("ai_profile_summary", e.target.value)
+ }
+ disabled={!isEditing}
+ rows={4}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+
+
+ {/* AI Fill Modal */}
+ {showAiModal && (
+
+
+
+ AI Profile Filling
+
+
+ Provide information about yourself, and AI will help fill in
+ your profile fields automatically.
+
+
setAiInput(e.target.value)}
+ placeholder="e.g., I'm a lifestyle content creator with 5 years of experience. I focus on sustainable living and wellness. I post 3 times a week on Instagram and have 50k followers..."
+ rows={6}
+ className="mb-4 w-full rounded-lg border border-gray-300 px-3 py-2"
+ />
+
+ {
+ setShowAiModal(false);
+ setAiInput("");
+ }}
+ className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+
+ {aiLoading ? (
+ <>
+
+ Generating...
+ >
+ ) : (
+ <>
+
+ Generate
+ >
+ )}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/app/creator/proposals/page.tsx b/frontend/app/creator/proposals/page.tsx
new file mode 100644
index 0000000..58f2a32
--- /dev/null
+++ b/frontend/app/creator/proposals/page.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import {
+ ProposalsWorkspace,
+ TabKey,
+} from "@/components/proposals/ProposalsWorkspace";
+import SlidingMenu from "@/components/SlidingMenu";
+import { useSearchParams } from "next/navigation";
+import { Suspense, useEffect, useMemo, useRef, useState } from "react";
+
+export default function CreatorProposalsPage() {
+ return (
+
+ Loading proposals...
+
+ }
+ >
+
+
+ );
+}
+
+function CreatorProposalsContent() {
+ const searchParams = useSearchParams();
+
+ const initialTab = useMemo(() => {
+ const section = searchParams?.get("section")?.toLowerCase();
+ if (section === "negotiations") {
+ return section as TabKey;
+ }
+ return "proposals";
+ }, [searchParams]);
+
+ // --- AI Review Progress Bar State ---
+ const [aiLoading, setAiLoading] = useState(false);
+ const [aiProgress, setAiProgress] = useState(0);
+ const [aiStatusIdx, setAiStatusIdx] = useState(0);
+ const aiStatusMessages = [
+ "Running initial checks...",
+ "Comparing with reference contracts...",
+ "Analyzing proposal structure...",
+ "Evaluating pricing fairness...",
+ "Checking for missing details...",
+ "Generating actionable feedback...",
+ ];
+ const aiIntervalRef = useRef(null);
+
+ useEffect(() => {
+ let progressInterval: NodeJS.Timeout | null = null;
+ if (aiLoading) {
+ setAiProgress(0);
+ setAiStatusIdx(0);
+ progressInterval = setInterval(() => {
+ setAiProgress((prev) => {
+ if (prev >= 100) return 100;
+ return prev + Math.floor(Math.random() * 5) + 2;
+ });
+ }, 300);
+ aiIntervalRef.current = setInterval(() => {
+ setAiStatusIdx((prev) => (prev + 1) % aiStatusMessages.length);
+ }, 3000);
+ } else {
+ setAiProgress(0);
+ setAiStatusIdx(0);
+ if (aiIntervalRef.current) clearInterval(aiIntervalRef.current);
+ if (progressInterval) clearInterval(progressInterval);
+ }
+ return () => {
+ if (aiIntervalRef.current) clearInterval(aiIntervalRef.current);
+ if (progressInterval) clearInterval(progressInterval);
+ };
+ // eslint-disable-next-line
+ }, [aiLoading]);
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/frontend/app/favicon.ico differ
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
new file mode 100644
index 0000000..5eccd1f
--- /dev/null
+++ b/frontend/app/globals.css
@@ -0,0 +1,87 @@
+@import "tailwindcss";
+
+:root {
+ --background: #ffffff;
+ --foreground: #171717;
+ --primary-50: #faf5ff;
+ --primary-100: #f3e8ff;
+ --primary-200: #e9d5ff;
+ --primary-300: #d8b4fe;
+ --primary-400: #c084fc;
+ --primary-500: #a855f7;
+ --primary-600: #9333ea;
+ --primary-700: #7e22ce;
+ --primary-800: #6b21a8;
+ --primary-900: #581c87;
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-primary-50: var(--primary-50);
+ --color-primary-100: var(--primary-100);
+ --color-primary-200: var(--primary-200);
+ --color-primary-300: var(--primary-300);
+ --color-primary-400: var(--primary-400);
+ --color-primary-500: var(--primary-500);
+ --color-primary-600: var(--primary-600);
+ --color-primary-700: var(--primary-700);
+ --color-primary-800: var(--primary-800);
+ --color-primary-900: var(--primary-900);
+ /* font variables removed: Geist fonts not used */
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0a0a0a;
+ --foreground: #ededed;
+ }
+}
+
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+/* Onboarding animations */
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.animate-fade-in-up {
+ animation: fadeInUp 0.5s ease-out;
+}
+
+/* Smooth transitions for inputs */
+input:focus,
+textarea:focus,
+select:focus {
+ transition: all 0.2s ease;
+}
+
+/* Custom scrollbar for better UX */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #888;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
new file mode 100644
index 0000000..83efb64
--- /dev/null
+++ b/frontend/app/layout.tsx
@@ -0,0 +1,18 @@
+
+import "./globals.css";
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx
new file mode 100644
index 0000000..697b092
--- /dev/null
+++ b/frontend/app/login/page.tsx
@@ -0,0 +1,227 @@
+"use client";
+
+import { getAuthErrorMessage } from "@/lib/auth-helpers";
+import { supabase } from "@/lib/supabaseClient";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Eye, EyeOff, Loader2, XCircle } from "lucide-react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+
+// Validation schema
+const loginSchema = z.object({
+ email: z.string().email("Invalid email address"),
+ password: z.string().min(1, "Password is required"),
+});
+
+type LoginFormData = z.infer;
+
+export default function LoginPage() {
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [error, setError] = useState(null);
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(loginSchema),
+ });
+
+ const onSubmit = async (data: LoginFormData) => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ // Step 1: Sign in with Supabase Auth
+ const { data: authData, error: authError } =
+ await supabase.auth.signInWithPassword({
+ email: data.email,
+ password: data.password,
+ });
+
+ if (authError) throw authError;
+
+ if (!authData.user) {
+ throw new Error("Failed to authenticate");
+ }
+
+ // Step 2: Fetch user's profile to get their role and onboarding status
+ const { data: profile, error: profileError } = await supabase
+ .from("profiles")
+ .select("role, name, onboarding_completed")
+ .eq("id", authData.user.id)
+ .single();
+
+ if (profileError) throw profileError;
+
+ if (!profile) {
+ throw new Error("User profile not found");
+ }
+
+ // Step 3: Redirect based on onboarding status and role
+ if (!profile.onboarding_completed) {
+ // User hasn't completed onboarding, redirect to onboarding flow
+ if (profile.role === "Creator") {
+ router.push("/creator/onboarding");
+ } else if (profile.role === "Brand") {
+ router.push("/brand/onboarding");
+ } else {
+ throw new Error("Invalid user role");
+ }
+ } else {
+ // User has completed onboarding, redirect to home
+ if (profile.role === "Creator") {
+ router.push("/creator/home");
+ } else if (profile.role === "Brand") {
+ router.push("/brand/home");
+ } else {
+ throw new Error("Invalid user role");
+ }
+ }
+ } catch (err: any) {
+ console.error("Login error:", err);
+ setError(getAuthErrorMessage(err));
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+
+ return (
+
+
+ {/* Soft background blobs */}
+
+
+ {/* Login Card */}
+
+
+
+ {/* Header */}
+
+
+ InPactAI
+
+
+ Welcome back! Please login to your account
+
+
+
+ {/* Error */}
+ {error && (
+
+ )}
+
+ {/* Form */}
+
+
+ {/* Email */}
+
+
+ Email address
+
+
+ {errors.email && (
+
+ {errors.email.message}
+
+ )}
+
+
+ {/* Password */}
+
+
+ Password
+
+
+
+ setShowPassword(!showPassword)}
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
+ >
+ {showPassword ? : }
+
+
+ {errors.password && (
+
+ {errors.password.message}
+
+ )}
+
+
+ {/* Button */}
+
+ {isLoading && }
+ {isLoading ? "Logging in..." : "Log In"}
+
+
+
+ {/* Footer */}
+
+ Don't have an account?{" "}
+
+ Sign up
+
+
+
+
+);
+
+
+}
\ No newline at end of file
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
new file mode 100644
index 0000000..5d2dd5e
--- /dev/null
+++ b/frontend/app/page.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { getCurrentUser, getUserProfile } from "@/lib/auth-helpers";
+import { Loader2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+export default function Home() {
+ const router = useRouter();
+
+ useEffect(() => {
+ async function checkAuthAndRedirect() {
+ try {
+ const user = await getCurrentUser();
+
+ if (user) {
+ // User is logged in, fetch their profile and redirect to appropriate home
+ const profile = await getUserProfile();
+ if (profile) {
+ if (profile.role === "Creator") {
+ router.push("/creator/home");
+ } else if (profile.role === "Brand") {
+ router.push("/brand/home");
+ }
+ } else {
+ // Profile not found, redirect to login
+ router.push("/login");
+ }
+ } else {
+ // User is not logged in, redirect to login
+ router.push("/login");
+ }
+ } catch (error) {
+ console.error("Error checking auth:", error);
+ router.push("/login");
+ }
+ }
+
+ checkAuthAndRedirect();
+ }, [router]);
+
+ return (
+
+ );
+}
diff --git a/frontend/app/signup/page.tsx b/frontend/app/signup/page.tsx
new file mode 100644
index 0000000..aab35d9
--- /dev/null
+++ b/frontend/app/signup/page.tsx
@@ -0,0 +1,325 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { CheckCircle2, Eye, EyeOff, Loader2, XCircle } from "lucide-react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+// Validation schema
+const signupSchema = z
+ .object({
+ name: z.string().min(2, "Name must be at least 2 characters"),
+ email: z.string().email("Invalid email address"),
+ accountType: z.enum(["Creator", "Brand"], {
+ message: "Please select an account type",
+ }),
+ password: z
+ .string()
+ .min(8, "Password must be at least 8 characters")
+ .regex(/[0-9]/, "Password must contain at least one number")
+ .regex(/[^a-zA-Z0-9]/, "Password must contain a special character"),
+ confirmPassword: z.string(),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords don't match",
+ path: ["confirmPassword"],
+ });
+
+type SignupFormData = z.infer;
+
+export default function SignupPage() {
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ // Allow error to be string or array of error objects
+ const [error, setError] = useState<
+ string | { msg?: string; detail?: string }[] | null
+ >(null);
+ const [success, setSuccess] = useState(false);
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(signupSchema),
+ });
+
+ const password = watch("password", "");
+
+ // Password strength calculation
+ const getPasswordStrength = (pwd: string) => {
+ if (!pwd) return { strength: 0, label: "", color: "" };
+
+ let strength = 0;
+ if (pwd.length >= 8) strength++;
+ if (pwd.length >= 12) strength++;
+ if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) strength++;
+ if (/[0-9]/.test(pwd)) strength++;
+ if (/[^a-zA-Z0-9]/.test(pwd)) strength++;
+
+ if (strength <= 2) return { strength, label: "Weak", color: "bg-red-500" };
+ if (strength <= 3)
+ return { strength, label: "Fair", color: "bg-yellow-500" };
+ if (strength <= 4) return { strength, label: "Good", color: "bg-blue-500" };
+ return { strength, label: "Strong", color: "bg-green-500" };
+ };
+
+ const passwordStrength = getPasswordStrength(password);
+
+ const onSubmit = async (data: SignupFormData) => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ // POST to FastAPI backend for atomic signup
+ const response = await fetch("/api/auth/signup", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: data.name,
+ email: data.email,
+ password: data.password,
+ role: data.accountType, // Send as 'Creator' or 'Brand' (case-sensitive)
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ // If errorData is an array (Pydantic validation error), set as array, else as string
+ if (Array.isArray(errorData)) {
+ setError(errorData);
+ } else {
+ setError(errorData?.detail || "Signup failed. Please try again.");
+ }
+ setIsLoading(false);
+ return;
+ }
+
+ // Success!
+ setSuccess(true);
+ setIsLoading(false);
+ // Redirect to login after 2 seconds
+ setTimeout(() => {
+ router.push("/login");
+ }, 2000);
+ } catch (err: any) {
+ console.error("Signup error:", err);
+ setError("Signup failed. Please try again.");
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ {/* soft background blobs */}
+
+
+
+ {/* Header */}
+
+
+ InPactAI
+
+
Create your account
+
+
+ {/* Success */}
+ {success && (
+
+
+
+
+ Account created successfully!
+
+
+ Redirecting to login page...
+
+
+
+ )}
+
+ {/* Error */}
+ {error && (
+
+
+
+ {Array.isArray(error)
+ ? error.map((e, i) => (
+
{e.msg || e.detail || JSON.stringify(e)}
+ ))
+ : error}
+
+
+ )}
+
+ {/* Signup Card */}
+
+
+ {/* Name */}
+
+
+ Full Name
+
+
+ {errors.name && (
+
+ {errors.name.message}
+
+ )}
+
+
+ {/* Email */}
+
+
+ Email Address
+
+
+ {errors.email && (
+
+ {errors.email.message}
+
+ )}
+
+
+ {/* Account Type */}
+
+
+ I am a…
+
+
+ Select account type
+ Creator
+ Brand
+
+ {errors.accountType && (
+
+ {errors.accountType.message}
+
+ )}
+
+
+ {/* Password */}
+
+
+ Password
+
+
+
+ setShowPassword(!showPassword)}
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
+ >
+ {showPassword ? : }
+
+
+
+
+ {/* Confirm Password */}
+
+
+ Confirm Password
+
+
+
+
+ setShowConfirmPassword(!showConfirmPassword)
+ }
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
+ >
+ {showConfirmPassword ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Submit */}
+
+ {isLoading && }
+ {isLoading ? "Creating Account..." : "Create Account"}
+
+
+
+
+ Already have an account?{" "}
+
+ Log in
+
+
+
+
+
+);
+
+}
diff --git a/frontend/components/SlidingMenu.tsx b/frontend/components/SlidingMenu.tsx
new file mode 100644
index 0000000..8985e65
--- /dev/null
+++ b/frontend/components/SlidingMenu.tsx
@@ -0,0 +1,267 @@
+"use client";
+import Link from "next/link";
+import { useEffect, useRef, useState } from "react";
+
+export type Role = "creator" | "brand";
+
+type Props = {
+ role: Role;
+};
+
+export default function SlidingMenu({ role }: Props) {
+ const [open, setOpen] = useState(false);
+ const panelRef = useRef(null);
+
+ // Close on ESC
+ useEffect(() => {
+ function onKey(e: KeyboardEvent) {
+ if (e.key === "Escape") setOpen(false);
+ }
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, []);
+
+ // Click outside to close
+ useEffect(() => {
+ function onClick(e: MouseEvent) {
+ if (!open) return;
+ if (!panelRef.current) return;
+ if (!panelRef.current.contains(e.target as Node)) setOpen(false);
+ }
+ document.addEventListener("mousedown", onClick);
+ return () => document.removeEventListener("mousedown", onClick);
+ }, [open]);
+
+ // Strict role comparison for reliable routing
+ const normalizedRole = String(role).trim().toLowerCase();
+ const basePath = normalizedRole === "brand" ? "/brand" : "/creator";
+ const createCampaignPath = `${basePath}/createcampaign`;
+ const proposalsPath = `${basePath}/proposals`;
+ const contractsPath = `${basePath}/contracts`;
+ const analyticsPath = `${basePath}/analytics`;
+
+ return (
+ <>
+ {/* Hamburger Button */}
+ setOpen((s) => !s)}
+ >
+ {/* simple hamburger icon */}
+
+
+
+
+
+ {/* Backdrop */}
+ setOpen(false)}
+ />
+
+ {/* Sliding panel */}
+
+ >
+ );
+}
diff --git a/frontend/components/analytics/AIAnalyticsDashboard.tsx b/frontend/components/analytics/AIAnalyticsDashboard.tsx
new file mode 100644
index 0000000..158100c
--- /dev/null
+++ b/frontend/components/analytics/AIAnalyticsDashboard.tsx
@@ -0,0 +1,1466 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ getPredictiveAnalytics,
+ getAutomatedInsights,
+ getAudienceSegmentation,
+ analyzeSentiment,
+ detectAnomalies,
+ getAttributionModeling,
+ getBenchmarking,
+ predictChurn,
+ naturalLanguageQuery,
+ getKPIOptimization,
+ type PredictiveAnalyticsResponse,
+ type AutomatedInsightsResponse,
+ type AudienceSegmentationResponse,
+ type SentimentAnalysisResponse,
+ type AnomalyDetectionResponse,
+ type AttributionModelingResponse,
+ type BenchmarkingResponse,
+ type ChurnPredictionResponse,
+ type NaturalLanguageQueryResponse,
+ type KPIOptimizationResponse,
+} from "@/lib/api/analytics";
+import {
+ TrendingUp,
+ Brain,
+ Users,
+ MessageSquare,
+ AlertTriangle,
+ BarChart3,
+ Target,
+ TrendingDown,
+ Search,
+ Lightbulb,
+ RefreshCw,
+ Sparkles,
+ Zap,
+ Activity,
+ PieChart,
+} from "lucide-react";
+
+interface AIAnalyticsDashboardProps {
+ campaignId?: string;
+ role?: "brand" | "creator";
+}
+
+export default function AIAnalyticsDashboard({
+ campaignId,
+ role = "brand",
+}: AIAnalyticsDashboardProps) {
+ const [activeTab, setActiveTab] = useState
("insights");
+ const [loading, setLoading] = useState>({});
+ const [error, setError] = useState>({});
+
+ // Data states
+ const [insights, setInsights] = useState(null);
+ const [predictive, setPredictive] = useState(null);
+ const [segmentation, setSegmentation] = useState(null);
+ const [sentiment, setSentiment] = useState(null);
+ const [sentimentText, setSentimentText] = useState("");
+ const [anomalies, setAnomalies] = useState(null);
+ const [attribution, setAttribution] = useState(null);
+ const [benchmarking, setBenchmarking] = useState(null);
+ const [churn, setChurn] = useState(null);
+ const [kpiOptimization, setKPIOptimization] = useState(null);
+
+ // Natural language query
+ const [nlQuery, setNlQuery] = useState("");
+ const [nlResponse, setNlResponse] = useState(null);
+ const [nlLoading, setNlLoading] = useState(false);
+
+ useEffect(() => {
+ loadInsights();
+ }, [campaignId]);
+
+ const setLoadingState = (key: string, value: boolean) => {
+ setLoading((prev) => ({ ...prev, [key]: value }));
+ };
+
+ const setErrorState = (key: string, value: string | null) => {
+ setError((prev) => ({ ...prev, [key]: value }));
+ };
+
+ const loadInsights = async () => {
+ setLoadingState("insights", true);
+ setErrorState("insights", null);
+ try {
+ const data = await getAutomatedInsights(campaignId);
+ setInsights(data);
+ } catch (err: any) {
+ setErrorState("insights", err.message || "Failed to load insights");
+ } finally {
+ setLoadingState("insights", false);
+ }
+ };
+
+ const loadPredictive = async () => {
+ setLoadingState("predictive", true);
+ setErrorState("predictive", null);
+ try {
+ const data = await getPredictiveAnalytics({
+ campaign_id: campaignId,
+ forecast_periods: 30,
+ });
+ setPredictive(data);
+ } catch (err: any) {
+ setErrorState("predictive", err.message || "Failed to load predictive analytics");
+ } finally {
+ setLoadingState("predictive", false);
+ }
+ };
+
+ const loadSegmentation = async () => {
+ setLoadingState("segmentation", true);
+ setErrorState("segmentation", null);
+ try {
+ const data = await getAudienceSegmentation(campaignId);
+ setSegmentation(data);
+ } catch (err: any) {
+ setErrorState("segmentation", err.message || "Failed to load audience segmentation");
+ } finally {
+ setLoadingState("segmentation", false);
+ }
+ };
+
+ const loadSentiment = async (customText?: string) => {
+ setLoadingState("sentiment", true);
+ setErrorState("sentiment", null);
+ try {
+ const data = await analyzeSentiment({
+ campaign_id: campaignId,
+ text: customText || sentimentText || undefined
+ });
+ setSentiment(data);
+ if (customText || sentimentText) {
+ setSentimentText(""); // Clear after successful analysis
+ }
+ } catch (err: any) {
+ setErrorState("sentiment", err.message || "Failed to analyze sentiment");
+ } finally {
+ setLoadingState("sentiment", false);
+ }
+ };
+
+ const loadAnomalies = async () => {
+ setLoadingState("anomalies", true);
+ setErrorState("anomalies", null);
+ try {
+ const data = await detectAnomalies(campaignId);
+ setAnomalies(data);
+ } catch (err: any) {
+ setErrorState("anomalies", err.message || "Failed to detect anomalies");
+ } finally {
+ setLoadingState("anomalies", false);
+ }
+ };
+
+ const loadAttribution = async () => {
+ setLoadingState("attribution", true);
+ setErrorState("attribution", null);
+ try {
+ const data = await getAttributionModeling(campaignId);
+ setAttribution(data);
+ } catch (err: any) {
+ setErrorState("attribution", err.message || "Failed to load attribution modeling");
+ } finally {
+ setLoadingState("attribution", false);
+ }
+ };
+
+ const loadBenchmarking = async () => {
+ setLoadingState("benchmarking", true);
+ setErrorState("benchmarking", null);
+ try {
+ const data = await getBenchmarking(campaignId);
+ setBenchmarking(data);
+ } catch (err: any) {
+ setErrorState("benchmarking", err.message || "Failed to load benchmarking");
+ } finally {
+ setLoadingState("benchmarking", false);
+ }
+ };
+
+ const loadChurn = async () => {
+ setLoadingState("churn", true);
+ setErrorState("churn", null);
+ try {
+ const data = await predictChurn(campaignId);
+ setChurn(data);
+ } catch (err: any) {
+ setErrorState("churn", err.message || "Failed to predict churn");
+ } finally {
+ setLoadingState("churn", false);
+ }
+ };
+
+ const loadKPIOptimization = async () => {
+ setLoadingState("kpi", true);
+ setErrorState("kpi", null);
+ try {
+ const data = await getKPIOptimization(campaignId);
+ setKPIOptimization(data);
+ } catch (err: any) {
+ setErrorState("kpi", err.message || "Failed to load KPI optimization");
+ } finally {
+ setLoadingState("kpi", false);
+ }
+ };
+
+ const handleNLQuery = async () => {
+ if (!nlQuery.trim()) return;
+ setNlLoading(true);
+ try {
+ const response = await naturalLanguageQuery({
+ query: nlQuery,
+ campaign_id: campaignId,
+ });
+ setNlResponse(response);
+ } catch (err: any) {
+ setErrorState("nl", err.message || "Failed to process query");
+ } finally {
+ setNlLoading(false);
+ }
+ };
+
+ const handleTabChange = (tab: string) => {
+ setActiveTab(tab);
+ // Load data when tab is first accessed
+ if (tab === "predictive" && !predictive) loadPredictive();
+ if (tab === "segmentation" && !segmentation) loadSegmentation();
+ if (tab === "sentiment" && !sentiment) loadSentiment();
+ if (tab === "anomalies" && !anomalies) loadAnomalies();
+ if (tab === "attribution" && !attribution) loadAttribution();
+ if (tab === "benchmarking" && !benchmarking) loadBenchmarking();
+ if (tab === "churn" && !churn) loadChurn();
+ if (tab === "kpi" && !kpiOptimization) loadKPIOptimization();
+ };
+
+ const tabs = [
+ { id: "insights", label: "Automated Insights", icon: Brain },
+ { id: "predictive", label: "Predictive Analytics", icon: TrendingUp },
+ { id: "segmentation", label: "Audience Segmentation", icon: Users },
+ { id: "sentiment", label: "Sentiment Analysis", icon: MessageSquare },
+ { id: "anomalies", label: "Anomaly Detection", icon: AlertTriangle },
+ { id: "attribution", label: "Attribution Modeling", icon: BarChart3 },
+ { id: "benchmarking", label: "Benchmarking", icon: Target },
+ { id: "churn", label: "Churn Prediction", icon: TrendingDown },
+ { id: "kpi", label: "KPI Optimization", icon: Zap },
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
AI-Powered Analytics
+
+ Advanced analytics powered by AI to help you make data-driven decisions
+
+
+
+
+ Powered by AI
+
+
+
+ {/* Natural Language Query */}
+
+
+
+
Ask Your Data
+
+
+ setNlQuery(e.target.value)}
+ onKeyPress={(e) => e.key === "Enter" && handleNLQuery()}
+ placeholder="Ask a question about your analytics data... (e.g., 'What's my average engagement rate?')"
+ className="flex-1 px-4 py-2 text-black border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ />
+
+ {nlLoading ? (
+
+ ) : (
+
+ )}
+ Ask
+
+
+ {nlResponse && (
+
+
{nlResponse.answer}
+
+ Confidence: {nlResponse.confidence}
+ {nlResponse.data_sources.length > 0 && (
+ Sources: {nlResponse.data_sources.join(", ")}
+ )}
+
+
+ )}
+
+
+ {/* Tabs */}
+
+
+ {tabs.map((tab) => {
+ const Icon = tab.icon;
+ return (
+ handleTabChange(tab.id)}
+ className={`flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm whitespace-nowrap ${
+ activeTab === tab.id
+ ? "border-purple-500 text-purple-600"
+ : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
+ }`}
+ >
+
+ {tab.label}
+
+ );
+ })}
+
+
+
+ {/* Tab Content */}
+
+ {activeTab === "insights" && (
+
+ )}
+ {activeTab === "predictive" && (
+
+ )}
+ {activeTab === "segmentation" && (
+
+ )}
+ {activeTab === "sentiment" && (
+
+ )}
+ {activeTab === "anomalies" && (
+
+ )}
+ {activeTab === "attribution" && (
+
+ )}
+ {activeTab === "benchmarking" && (
+
+ )}
+ {activeTab === "churn" && (
+
+ )}
+ {activeTab === "kpi" && (
+
+ )}
+
+
+ );
+}
+
+// Tab Components
+function InsightsTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: AutomatedInsightsResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Automated Insights
+
+
+ Refresh
+
+
+
+
+
Executive Summary
+
{data.summary}
+
+
+
+
+
+
+ Key Trends
+
+
+ {data.trends.map((trend, idx) => (
+
+ •
+ {trend}
+
+ ))}
+
+
+
+
+
+
+ Recommendations
+
+
+ {data.recommendations.map((rec, idx) => (
+
+ •
+ {rec}
+
+ ))}
+
+
+
+
+ {data.anomalies.length > 0 && (
+
+
+
+ Detected Anomalies
+
+
+ {data.anomalies.map((anomaly, idx) => (
+
+
{anomaly.metric}
+
{anomaly.description}
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+function PredictiveTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: PredictiveAnalyticsResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Predictive Analytics
+
+
+ Refresh
+
+
+
+
+
+
Predicted Value
+
+ {data.forecast.predicted_value.toFixed(2)}
+
+
+
+
Growth Rate
+
+ {(data.forecast.growth_rate * 100).toFixed(1)}%
+
+
+
+
Confidence
+
{data.confidence}
+
+
+
+
+
Forecasted Values
+
+ {data.forecast.forecasted_values.slice(0, 10).map((fv, idx) => (
+
+ {fv.date}
+ {fv.value.toFixed(2)}
+
+ ))}
+
+
+
+
+
Key Factors
+
+ {data.factors.map((factor, idx) => (
+
+ •
+ {factor}
+
+ ))}
+
+
+
+
+
Recommendations
+
+ {data.recommendations.map((rec, idx) => (
+
+ •
+ {rec}
+
+ ))}
+
+
+
+ );
+}
+
+function SegmentationTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: AudienceSegmentationResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Audience Segmentation
+
+
+ Refresh
+
+
+
+
+ {data.segments.map((segment, idx) => (
+
+
+
{segment.name}
+ {segment.size}%
+
+
+
Characteristics:
+
+ {segment.characteristics.map((char, charIdx) => (
+
+ •
+ {char}
+
+ ))}
+
+ {segment.engagement_score !== undefined && (
+
+
Engagement Score
+
+ {(segment.engagement_score * 100).toFixed(1)}%
+
+
+ )}
+
+
+ ))}
+
+
+ );
+}
+
+function SentimentTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+ sentimentText,
+ setSentimentText,
+ onAnalyzeText,
+}: {
+ data: SentimentAnalysisResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+ sentimentText: string;
+ setSentimentText: (text: string) => void;
+ onAnalyzeText: (text?: string) => void;
+}) {
+ if (loading)
+ return (
+
+
+
Sentiment Analysis
+
+ {/* Text Input Section - Show even while loading */}
+
+
Analyze Custom Text
+
+ Paste text from social media comments, reviews, or feedback to analyze sentiment
+
+
+
setSentimentText(e.target.value)}
+ placeholder="Paste text here to analyze sentiment... (e.g., social media comments, reviews, feedback)"
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
+ rows={4}
+ />
+
+ onAnalyzeText(sentimentText)}
+ disabled={loading || !sentimentText.trim()}
+ className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
+ >
+
+ Analyzing...
+
+ {sentimentText && (
+ setSentimentText("")}
+ className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg hover:bg-gray-50"
+ >
+ Clear
+
+ )}
+
+
+
+
+
+
+
+ );
+ if (error)
+ return (
+
+
+
Sentiment Analysis
+
+
+ Refresh
+
+
+ {/* Text Input Section - Show even on error */}
+
+
Analyze Custom Text
+
+ Paste text from social media comments, reviews, or feedback to analyze sentiment
+
+
+
setSentimentText(e.target.value)}
+ placeholder="Paste text here to analyze sentiment... (e.g., social media comments, reviews, feedback)"
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
+ rows={4}
+ />
+
+ onAnalyzeText(sentimentText)}
+ disabled={loading || !sentimentText.trim()}
+ className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
+ >
+
+ Analyze Text
+
+ {sentimentText && (
+ setSentimentText("")}
+ className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg hover:bg-gray-50"
+ >
+ Clear
+
+ )}
+
+
+
+
+
+ );
+ if (!data) {
+ return (
+
+
+
Sentiment Analysis
+
+
+ Refresh
+
+
+ {/* Text Input Section */}
+
+
Analyze Custom Text
+
+ Paste text from social media comments, reviews, or feedback to analyze sentiment
+
+
+
setSentimentText(e.target.value)}
+ placeholder="Paste text here to analyze sentiment... (e.g., social media comments, reviews, feedback)"
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
+ rows={4}
+ />
+
+ onAnalyzeText(sentimentText)}
+ disabled={loading || !sentimentText.trim()}
+ className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
+ >
+
+ Analyze Text
+
+ {sentimentText && (
+ setSentimentText("")}
+ className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg hover:bg-gray-50"
+ >
+ Clear
+
+ )}
+
+
+
+ Or click "Refresh" above to analyze feedback from your campaign metrics
+
+
+
+ );
+ }
+
+ const sentimentColor =
+ data.overall_sentiment === "positive"
+ ? "green"
+ : data.overall_sentiment === "negative"
+ ? "red"
+ : "gray";
+
+ return (
+
+
+
Sentiment Analysis
+
+
+ Refresh
+
+
+
+ {/* Text Input Section */}
+
+
Analyze Custom Text
+
+ Paste text from social media comments, reviews, or feedback to analyze sentiment
+
+
+
setSentimentText(e.target.value)}
+ placeholder="Paste text here to analyze sentiment... (e.g., social media comments, reviews, feedback)"
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
+ rows={4}
+ />
+
+ onAnalyzeText(sentimentText)}
+ disabled={loading || !sentimentText.trim()}
+ className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
+ >
+ {loading ? (
+
+ ) : (
+
+ )}
+ Analyze Text
+
+ {sentimentText && (
+ setSentimentText("")}
+ className="px-4 py-2 text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg hover:bg-gray-50"
+ >
+ Clear
+
+ )}
+
+
+
+ Or click "Refresh" above to analyze feedback from your campaign metrics
+
+
+
+
+
+
Overall Sentiment
+
+ {data.overall_sentiment}
+
+
Score: {data.sentiment_score.toFixed(2)}
+
+
+
Sentiment Score
+
+
0 ? "bg-green-500" : "bg-red-500"
+ }`}
+ style={{ width: `${Math.abs(data.sentiment_score) * 100}%` }}
+ />
+
+
+
+
+
+
+
Positive Aspects
+
+ {data.positive_aspects.map((aspect, idx) => (
+
+ •
+ {aspect}
+
+ ))}
+
+
+
+
Negative Aspects
+
+ {data.negative_aspects.map((aspect, idx) => (
+
+ •
+ {aspect}
+
+ ))}
+
+
+
+
+
+
Recommendations
+
+ {data.recommendations.map((rec, idx) => (
+
+ •
+ {rec}
+
+ ))}
+
+
+
+ );
+}
+
+function AnomaliesTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: AnomalyDetectionResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Anomaly Detection
+
+
+ Refresh
+
+
+
+
+
Summary
+
{data.summary}
+
+
+ {data.anomalies.length > 0 ? (
+
+ {data.anomalies.map((anomaly, idx) => (
+
+
+
{anomaly.metric}
+
+ {anomaly.severity}
+
+
+
Date: {anomaly.date}
+
{anomaly.description}
+
+
+
Value
+
{anomaly.value.toFixed(2)}
+
+
+
Expected
+
{anomaly.expected_value.toFixed(2)}
+
+
+
Deviation
+
{anomaly.deviation.toFixed(2)}
+
+
+
+ ))}
+
+ ) : (
+
+
No anomalies detected. Your metrics are within normal ranges.
+
+ )}
+
+ );
+}
+
+function AttributionTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: AttributionModelingResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Attribution Modeling
+
+
+ Refresh
+
+
+
+
+
Attribution Breakdown
+
+ {Object.entries(data.attribution).map(([channel, percent]) => (
+
+
+ {channel}
+ {percent.toFixed(1)}%
+
+
+
+ ))}
+
+
+
+
+
Top Contributors
+
+ {data.top_contributors.map((contributor, idx) => (
+
+
+
{contributor.name}
+
+ {contributor.contribution_percent.toFixed(1)}%
+
+
+
+ Total Value: {contributor.total_value.toFixed(2)}
+
+
{contributor.insight}
+
+ ))}
+
+
+
+
+
Insights
+
+ {data.insights.map((insight, idx) => (
+
+ •
+ {insight}
+
+ ))}
+
+
+
+ );
+}
+
+function BenchmarkingTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: BenchmarkingResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Industry Benchmarking
+
+
+ Refresh
+
+
+
+
+ {Object.entries(data.comparison).map(([metric, comp]) => (
+
+
{metric}
+
+
+ Your Value
+ {comp.your_value.toFixed(2)}
+
+
+ Industry Average
+ {comp.industry_avg.toFixed(2)}
+
+
+ Percentile
+ {comp.percentile}th
+
+
+
+ {comp.status} average
+
+
+
+
+ ))}
+
+
+
+
Recommendations
+
+ {data.recommendations.map((rec, idx) => (
+
+ •
+ {rec}
+
+ ))}
+
+
+
+ );
+}
+
+function ChurnTab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: ChurnPredictionResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
Churn Prediction
+
+
+ Refresh
+
+
+
+ {data.at_risk_segments.length > 0 ? (
+
+ {data.at_risk_segments.map((segment, idx) => (
+
+
+
{segment.segment}
+
+ {(segment.risk_score * 100).toFixed(0)}% Risk
+
+
+
+
+
Indicators
+
+ {segment.indicators.map((indicator, indIdx) => (
+
+ •
+ {indicator}
+
+ ))}
+
+
+
+
Recommendations
+
+ {segment.recommendations.map((rec, recIdx) => (
+
+ •
+ {rec}
+
+ ))}
+
+
+
+
+ ))}
+
+ ) : (
+
+
No high-risk segments detected. Your audience engagement is stable.
+
+ )}
+
+
+
General Recommendations
+
+ {data.recommendations.map((rec, idx) => (
+
+ •
+ {rec}
+
+ ))}
+
+
+
+ );
+}
+
+function KPITab({
+ data,
+ loading,
+ error,
+ onRefresh,
+}: {
+ data: KPIOptimizationResponse | null;
+ loading: boolean;
+ error: string | null;
+ onRefresh: () => void;
+}) {
+ if (loading)
+ return (
+
+
+
+ );
+ if (error)
+ return (
+
+ );
+ if (!data) return null;
+
+ return (
+
+
+
KPI Optimization
+
+
+ Refresh
+
+
+
+
+
Current KPIs
+
+ {Object.entries(data.current_kpis).map(([kpi, value]) => (
+
+
{kpi}
+
{value.toFixed(2)}
+
+ ))}
+
+
+
+
+ {data.optimization_suggestions.map((suggestion, idx) => (
+
+
+
{suggestion.kpi}
+
+ {suggestion.expected_impact} impact
+
+
+
+
+
Current Value
+
+ {suggestion.current_value.toFixed(2)}
+
+
+
+
Target Value
+
+ {suggestion.target_value.toFixed(2)}
+
+
+
+
+
Suggestions
+
+ {suggestion.suggestions.map((sug, sugIdx) => (
+
+ •
+ {sug}
+
+ ))}
+
+
+
+ ))}
+
+
+
+
Priority Actions
+
+ {data.priority_actions.map((action, idx) => (
+
+ {idx + 1}.
+ {action}
+
+ ))}
+
+
+
+ );
+}
+
diff --git a/frontend/components/analytics/BrandAnalyticsDashboard.tsx b/frontend/components/analytics/BrandAnalyticsDashboard.tsx
new file mode 100644
index 0000000..59268d2
--- /dev/null
+++ b/frontend/components/analytics/BrandAnalyticsDashboard.tsx
@@ -0,0 +1,644 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ getAllBrandDeliverables,
+ createMetric,
+ createUpdateRequest,
+ createFeedback,
+ getDeliverableMetrics,
+ AllDeliverablesResponse,
+} from "@/lib/api/analytics";
+import type {
+ CampaignDeliverableMetric,
+ MetricUpdate,
+} from "@/types/analytics";
+import {
+ Plus,
+ RefreshCw,
+ AlertCircle,
+ Target,
+ TrendingUp,
+ Package,
+ MessageSquare,
+ Send,
+ X,
+ Edit,
+ Trash2,
+} from "lucide-react";
+
+export default function BrandAnalyticsDashboard() {
+ const [data, setData] = useState
(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [selectedCampaign, setSelectedCampaign] = useState(null);
+ const [selectedDeliverable, setSelectedDeliverable] = useState(null);
+ const [showMetricModal, setShowMetricModal] = useState(false);
+ const [showRequestModal, setShowRequestModal] = useState(false);
+ const [showFeedbackModal, setShowFeedbackModal] = useState(false);
+ const [selectedMetric, setSelectedMetric] =
+ useState(null);
+ const [selectedUpdate, setSelectedUpdate] = useState(null);
+ const [feedbackText, setFeedbackText] = useState("");
+ const [selectedCreatorId, setSelectedCreatorId] = useState(null);
+ const [newMetric, setNewMetric] = useState({
+ name: "",
+ display_name: "",
+ target_value: "",
+ is_custom: false,
+ });
+
+ useEffect(() => {
+ loadData();
+ }, []);
+
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const result = await getAllBrandDeliverables();
+ setData(result);
+ } catch (err: any) {
+ setError(err.message || "Failed to load deliverables");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCreateMetric = async () => {
+ if (!selectedDeliverable || !newMetric.name) return;
+ try {
+ await createMetric({
+ campaign_deliverable_id: selectedDeliverable.id,
+ name: newMetric.name,
+ display_name: newMetric.display_name || newMetric.name,
+ target_value: newMetric.target_value
+ ? parseFloat(newMetric.target_value)
+ : undefined,
+ is_custom: newMetric.is_custom,
+ });
+ setShowMetricModal(false);
+ setNewMetric({ name: "", display_name: "", target_value: "", is_custom: false });
+ await loadData();
+ } catch (err: any) {
+ setError(err.message || "Failed to create metric");
+ }
+ };
+
+ const handleRequestUpdate = async () => {
+ if (!selectedDeliverable || !selectedCreatorId) return;
+ try {
+ await createUpdateRequest({
+ campaign_deliverable_metric_id: selectedMetric?.id,
+ creator_id: selectedCreatorId,
+ });
+ setShowRequestModal(false);
+ setSelectedCreatorId(null);
+ await loadData();
+ } catch (err: any) {
+ setError(err.message || "Failed to request update");
+ }
+ };
+
+ const handleCreateFeedback = async () => {
+ if (!selectedUpdate) return;
+ try {
+ await createFeedback(selectedUpdate.id, {
+ feedback_text: feedbackText,
+ });
+ setShowFeedbackModal(false);
+ setFeedbackText("");
+ await loadData();
+ } catch (err: any) {
+ setError(err.message || "Failed to create feedback");
+ }
+ };
+
+ const filteredDeliverables = data?.deliverables.filter((deliverable) => {
+ if (!selectedCampaign) return true;
+ return deliverable.campaign_id === selectedCampaign;
+ }) || [];
+
+ if (loading && !data) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+ Campaign Analytics Dashboard
+
+
+ Define metrics, track performance, and request updates from creators
+
+
+
+
+ Refresh
+
+
+
+ {/* Summary Cards */}
+ {data && (
+
+
+
+
+
Total Deliverables
+
+ {data.total_deliverables}
+
+
+
+
+
+
+
+
+
Total Metrics
+
+ {data.total_metrics}
+
+
+
+
+
+
+
+
+
Metrics Updated
+
+ {data.metrics_with_updates}
+
+
+
+
+
+
+
+
+
Active Campaigns
+
+ {data.campaigns.length}
+
+
+
+
+
+
+ )}
+
+ {/* Campaign Filter */}
+ {data && data.campaigns.length > 0 && (
+
+
+ Filter by Campaign
+
+ setSelectedCampaign(e.target.value || null)}
+ className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-blue-500 focus:outline-none"
+ >
+ All Campaigns
+ {data.campaigns.map((campaign) => (
+
+ {campaign.title}
+
+ ))}
+
+
+ )}
+
+ {error && (
+
+ )}
+
+ {/* Deliverables List */}
+ {!data || filteredDeliverables.length === 0 ? (
+
+
+ {!data
+ ? "Loading deliverables..."
+ : "No deliverables found. Create a campaign with deliverables to start tracking metrics."}
+
+
+ ) : (
+
+ {filteredDeliverables.map((deliverable) => (
+
+
+
+
+
+ {deliverable.content_type}
+
+
+ {deliverable.platform}
+
+
+
+ Campaign: {deliverable.campaign?.title || "Unknown"}
+ Quantity: {deliverable.quantity}
+
+ {deliverable.guidance && (
+
{deliverable.guidance}
+ )}
+
+
{
+ setSelectedDeliverable(deliverable);
+ setShowMetricModal(true);
+ }}
+ className="ml-4 flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
+ >
+
+ Add Metric
+
+
+
+ {/* Metrics */}
+
+
+ Metrics ({deliverable.metrics.length})
+
+ {deliverable.metrics.length === 0 ? (
+
+ No metrics defined. Add one to start tracking.
+
+ ) : (
+
+ {deliverable.metrics.map((metric) => (
+ {
+ setSelectedDeliverable(deliverable);
+ setSelectedMetric(metric);
+ setShowRequestModal(true);
+ }}
+ onAddFeedback={(update) => {
+ setSelectedUpdate(update);
+ setShowFeedbackModal(true);
+ }}
+ />
+ ))}
+
+ )}
+
+
+ ))}
+
+ )}
+
+ {/* Create Metric Modal */}
+ {showMetricModal && selectedDeliverable && (
+
+
+
+
Create New Metric
+ setShowMetricModal(false)}
+ className="rounded-lg bg-gray-100 p-2 hover:bg-gray-200"
+ >
+
+
+
+
+
+
+ Metric Name *
+
+
+ setNewMetric({ ...newMetric, name: e.target.value })
+ }
+ placeholder="e.g., impressions, likes, views"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none"
+ />
+
+
+
+ Display Name
+
+
+ setNewMetric({
+ ...newMetric,
+ display_name: e.target.value,
+ })
+ }
+ placeholder="e.g., Total Impressions"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none"
+ />
+
+
+
+ Target Value
+
+
+ setNewMetric({
+ ...newMetric,
+ target_value: e.target.value,
+ })
+ }
+ placeholder="e.g., 10000"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none"
+ />
+
+
+
+ setNewMetric({
+ ...newMetric,
+ is_custom: e.target.checked,
+ })
+ }
+ className="h-4 w-4 rounded border-gray-300 text-blue-600"
+ />
+ Custom Metric
+
+
+ setShowMetricModal(false)}
+ className="flex-1 rounded-lg border border-gray-300 bg-white px-4 py-2 text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+
+ Create
+
+
+
+
+
+ )}
+
+ {/* Request Update Modal */}
+ {showRequestModal && selectedDeliverable && selectedMetric && (
+
+
+
+
Request Metric Update
+ {
+ setShowRequestModal(false);
+ setSelectedCreatorId(null);
+ }}
+ className="rounded-lg bg-gray-100 p-2 hover:bg-gray-200"
+ >
+
+
+
+
+
+ Request creator to update: {selectedMetric.display_name || selectedMetric.name}
+
+ {selectedDeliverable.campaign?.creators &&
+ selectedDeliverable.campaign.creators.length > 0 ? (
+
+
+ Select Creator
+
+
+ {selectedDeliverable.campaign.creators.map((creator: any) => (
+ setSelectedCreatorId(creator.id)}
+ className={`w-full rounded-lg border px-4 py-2 text-left transition-colors ${
+ selectedCreatorId === creator.id
+ ? "border-blue-500 bg-blue-50"
+ : "border-gray-300 bg-white hover:bg-gray-50"
+ }`}
+ >
+ {creator.display_name || "Unknown Creator"}
+
+ ))}
+
+
+ {
+ setShowRequestModal(false);
+ setSelectedCreatorId(null);
+ }}
+ className="flex-1 rounded-lg border border-gray-300 bg-white px-4 py-2 text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+
+ Send Request
+
+
+
+ ) : (
+
+ No creators found for this campaign.
+
+ )}
+
+
+
+ )}
+
+ {/* Feedback Modal */}
+ {showFeedbackModal && selectedUpdate && (
+
+
+
+
Add Feedback
+ setShowFeedbackModal(false)}
+ className="rounded-lg bg-gray-100 p-2 hover:bg-gray-200"
+ >
+
+
+
+
+
+
+ Feedback
+
+ setFeedbackText(e.target.value)}
+ rows={4}
+ placeholder="Provide feedback on this metric update..."
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none"
+ />
+
+
+ {
+ setShowFeedbackModal(false);
+ setFeedbackText("");
+ }}
+ className="flex-1 rounded-lg border border-gray-300 bg-white px-4 py-2 text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+
+ Submit
+
+
+
+
+
+ )}
+
+ );
+}
+
+function MetricCard({
+ metric,
+ deliverable,
+ onRequestUpdate,
+ onAddFeedback,
+}: {
+ metric: CampaignDeliverableMetric;
+ deliverable: any;
+ onRequestUpdate: () => void;
+ onAddFeedback: (update: MetricUpdate) => void;
+}) {
+ const progress =
+ metric.target_value && metric.latest_update
+ ? (metric.latest_update.value / metric.target_value) * 100
+ : 0;
+
+ const formatNumber = (num: number) => {
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
+ if (num >= 1000) return (num / 1000).toFixed(1) + "K";
+ return num.toString();
+ };
+
+ return (
+
+
+
+
+ {metric.display_name || metric.name}
+
+ {metric.is_custom && (
+
+ Custom
+
+ )}
+
+
+
+ {metric.latest_update ? (
+
+
+ Current Value
+
+ {formatNumber(metric.latest_update.value)}
+
+
+ {metric.target_value && (
+
+
+
+ Target: {formatNumber(metric.target_value)}
+
+ = 100
+ ? "text-green-600"
+ : progress >= 50
+ ? "text-yellow-600"
+ : "text-red-600"
+ }`}
+ >
+ {progress.toFixed(1)}%
+
+
+
+
= 100
+ ? "bg-green-500"
+ : progress >= 50
+ ? "bg-yellow-500"
+ : "bg-red-500"
+ }`}
+ style={{ width: `${Math.min(progress, 100)}%` }}
+ />
+
+
+ )}
+
+ ) : (
+
+
+
+ No data submitted yet
+
+
+ )}
+
+
+ {deliverable.campaign?.creators &&
+ deliverable.campaign.creators.length > 0 && (
+
+
+ Request Update
+
+ )}
+ {metric.latest_update && (
+ onAddFeedback(metric.latest_update!)}
+ className="flex items-center gap-1 rounded bg-purple-50 px-3 py-1.5 text-xs font-medium text-purple-700 hover:bg-purple-100"
+ >
+
+ Add Feedback
+
+ )}
+
+
+ {metric.latest_feedback && (
+
+
Your Feedback:
+
{metric.latest_feedback.feedback_text}
+
+ )}
+
+ );
+}
+
diff --git a/frontend/components/analytics/CreatorAnalyticsDashboard.tsx b/frontend/components/analytics/CreatorAnalyticsDashboard.tsx
new file mode 100644
index 0000000..46a1840
--- /dev/null
+++ b/frontend/components/analytics/CreatorAnalyticsDashboard.tsx
@@ -0,0 +1,166 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ getCreatorCampaigns,
+ CreatorCampaign,
+} from "@/lib/api/analytics";
+import {
+ TrendingUp,
+ DollarSign,
+ Building2,
+ RefreshCw,
+ AlertCircle,
+ ArrowRight,
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+
+export default function CreatorAnalyticsDashboard() {
+ const [campaigns, setCampaigns] = useState
([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const router = useRouter();
+
+ useEffect(() => {
+ loadCampaigns();
+ }, []);
+
+ const loadCampaigns = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const result = await getCreatorCampaigns();
+ setCampaigns(result.campaigns);
+ } catch (err: any) {
+ setError(err.message || "Failed to load campaigns");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat("en-IN", {
+ style: "currency",
+ currency: "INR",
+ maximumFractionDigits: 0,
+ }).format(amount);
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status?.toLowerCase()) {
+ case "active":
+ return "bg-green-100 text-green-800";
+ case "completed":
+ return "bg-blue-100 text-blue-800";
+ case "pending":
+ return "bg-yellow-100 text-yellow-800";
+ default:
+ return "bg-gray-100 text-gray-800";
+ }
+ };
+
+ if (loading && campaigns.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
My Campaigns
+
+ Track your campaign progress and deliverables
+
+
+
+
+ Refresh
+
+
+
+ {error && (
+
+ )}
+
+ {/* Campaigns Grid */}
+ {campaigns.length === 0 ? (
+
+
+ No campaigns found. You'll see your campaigns here once you accept proposals.
+
+
+ ) : (
+
+ {campaigns.map((campaign) => (
+
router.push(`/creator/analytics/${campaign.id}`)}
+ className="cursor-pointer rounded-xl bg-white p-6 shadow-md transition-all hover:shadow-lg hover:scale-105"
+ >
+
+
+
+ {campaign.title}
+
+
+
+ {campaign.brand_name || "Unknown Brand"}
+
+
+
+ {campaign.contract_status || campaign.status}
+
+
+
+
+
+ Value
+
+
+ {formatCurrency(campaign.value)}
+
+
+
+
+
+ Progress
+
+ {campaign.progress.toFixed(1)}%
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
+
diff --git a/frontend/components/analytics/CreatorCampaignDetailsView.tsx b/frontend/components/analytics/CreatorCampaignDetailsView.tsx
new file mode 100644
index 0000000..d649575
--- /dev/null
+++ b/frontend/components/analytics/CreatorCampaignDetailsView.tsx
@@ -0,0 +1,210 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import {
+ getCreatorCampaignDetails,
+ getPlatformDeliverables,
+ CreatorCampaignDetails,
+ PlatformDeliverablesResponse,
+} from "@/lib/api/analytics";
+import {
+ ArrowLeft,
+ RefreshCw,
+ AlertCircle,
+ Package,
+ TrendingUp,
+ CheckCircle2,
+ Clock,
+} from "lucide-react";
+import PlatformDeliverablesModal from "./PlatformDeliverablesModal";
+import DeliverableMetricsModal from "./DeliverableMetricsModal";
+
+interface CreatorCampaignDetailsViewProps {
+ campaignId: string;
+}
+
+export default function CreatorCampaignDetailsView({
+ campaignId,
+}: CreatorCampaignDetailsViewProps) {
+ const [campaign, setCampaign] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [selectedPlatform, setSelectedPlatform] = useState(null);
+ const [selectedDeliverableId, setSelectedDeliverableId] = useState<
+ string | null
+ >(null);
+ const [platformDeliverables, setPlatformDeliverables] = useState<
+ PlatformDeliverablesResponse | null
+ >(null);
+ const router = useRouter();
+
+ useEffect(() => {
+ loadCampaignDetails();
+ }, [campaignId]);
+
+ const loadCampaignDetails = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const data = await getCreatorCampaignDetails(campaignId);
+ setCampaign(data);
+ } catch (err: any) {
+ setError(err.message || "Failed to load campaign details");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handlePlatformClick = async (platform: string) => {
+ try {
+ setSelectedPlatform(platform);
+ const data = await getPlatformDeliverables(campaignId, platform);
+ setPlatformDeliverables(data);
+ } catch (err: any) {
+ setError(err.message || "Failed to load platform deliverables");
+ }
+ };
+
+ const handleDeliverableClick = (deliverableId: string) => {
+ setSelectedDeliverableId(deliverableId);
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status?.toLowerCase()) {
+ case "completed":
+ return ;
+ case "pending":
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ if (loading && !campaign) {
+ return (
+
+
+
+ );
+ }
+
+ if (!campaign) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
router.push("/creator/analytics")}
+ className="rounded-lg bg-gray-100 p-2 hover:bg-gray-200"
+ >
+
+
+
+
+ {campaign.title}
+
+
+ Brand: {campaign.brand_name || "Unknown"}
+
+
+
+
+
+ Refresh
+
+
+
+ {error && (
+
+ )}
+
+ {/* Platforms Grid */}
+ {campaign.platforms.length === 0 ? (
+
+
No deliverables found for this campaign.
+
+ ) : (
+
+ {campaign.platforms.map((platform) => (
+
handlePlatformClick(platform.platform)}
+ className="cursor-pointer rounded-xl bg-white p-6 shadow-md transition-all hover:shadow-lg hover:scale-105"
+ >
+
+
+ {platform.platform}
+
+ {getStatusIcon(
+ platform.completed === platform.total
+ ? "completed"
+ : "pending"
+ )}
+
+
+
+
+ Deliverables
+
+ {platform.completed}/{platform.total}
+
+
+
+
+ Progress
+ {platform.progress.toFixed(1)}%
+
+
+
+
+ Click to view deliverables →
+
+
+ ))}
+
+ )}
+
+ {/* Platform Deliverables Modal */}
+ {selectedPlatform && platformDeliverables && (
+
{
+ setSelectedPlatform(null);
+ setPlatformDeliverables(null);
+ }}
+ onDeliverableClick={handleDeliverableClick}
+ />
+ )}
+
+ {/* Deliverable Metrics Modal */}
+ {selectedDeliverableId && (
+ setSelectedDeliverableId(null)}
+ />
+ )}
+
+ );
+}
+
diff --git a/frontend/components/analytics/DeliverableMetricsModal.tsx b/frontend/components/analytics/DeliverableMetricsModal.tsx
new file mode 100644
index 0000000..8d1af9b
--- /dev/null
+++ b/frontend/components/analytics/DeliverableMetricsModal.tsx
@@ -0,0 +1,366 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ X,
+ TrendingUp,
+ Target,
+ MessageSquare,
+ Send,
+ RefreshCw,
+ AlertCircle,
+ CheckCircle2,
+} from "lucide-react";
+import {
+ getCreatorDeliverableMetrics,
+ submitMetricValue,
+ createCreatorComment,
+} from "@/lib/api/analytics";
+import type {
+ DeliverableMetricsResponse,
+ CampaignDeliverableMetric,
+} from "@/types/analytics";
+
+interface DeliverableMetricsModalProps {
+ deliverableId: string;
+ onClose: () => void;
+}
+
+export default function DeliverableMetricsModal({
+ deliverableId,
+ onClose,
+}: DeliverableMetricsModalProps) {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [selectedMetric, setSelectedMetric] = useState(null);
+ const [metricValue, setMetricValue] = useState("");
+ const [commentText, setCommentText] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ useEffect(() => {
+ loadMetrics();
+ }, [deliverableId]);
+
+ const loadMetrics = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const result = await getCreatorDeliverableMetrics(deliverableId);
+ setData(result);
+ } catch (err: any) {
+ setError(err.message || "Failed to load metrics");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSubmitMetric = async (metricId: string) => {
+ if (!metricValue || isNaN(parseFloat(metricValue))) {
+ setError("Please enter a valid number");
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ setError(null);
+ await submitMetricValue(metricId, {
+ value: parseFloat(metricValue),
+ });
+ setMetricValue("");
+ setSelectedMetric(null);
+ await loadMetrics();
+ } catch (err: any) {
+ setError(err.message || "Failed to submit metric value");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleSubmitComment = async (metricId: string) => {
+ if (!commentText.trim()) {
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ setError(null);
+ await createCreatorComment(metricId, {
+ comment_text: commentText,
+ });
+ setCommentText("");
+ await loadMetrics();
+ } catch (err: any) {
+ setError(err.message || "Failed to add comment");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const formatNumber = (num: number) => {
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
+ if (num >= 1000) return (num / 1000).toFixed(1) + "K";
+ return num.toString();
+ };
+
+ const calculateProgress = (metric: CampaignDeliverableMetric): number => {
+ if (!metric.target_value || !metric.latest_update) return 0;
+ return (metric.latest_update.value / metric.target_value) * 100;
+ };
+
+ const getCreatorComments = (metric: CampaignDeliverableMetric) => {
+ if (!metric.latest_update?.demographics) return [];
+ const comments = metric.latest_update.demographics.creator_comments;
+ return Array.isArray(comments) ? comments : [];
+ };
+
+ if (loading && !data) {
+ return (
+
+ );
+ }
+
+ if (!data) {
+ return null;
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
Deliverable Metrics
+
+ {data.deliverable.description}
+
+
+
+
+
+
+
+ {/* Content */}
+
+ {error && (
+
+ )}
+
+ {data.metrics.length === 0 ? (
+
+
+
No metrics defined for this deliverable.
+
+ ) : (
+
+ {data.metrics.map((metric) => {
+ const progress = calculateProgress(metric);
+ const creatorComments = getCreatorComments(metric);
+ const isSelected = selectedMetric === metric.id;
+
+ return (
+
+ {/* Metric Header */}
+
+
+
+ {metric.display_name || metric.name}
+
+
{metric.name}
+
+ {metric.is_custom && (
+
+ Custom
+
+ )}
+
+
+ {/* Current Value */}
+ {metric.latest_update ? (
+
+
+
+ Current Value
+
+
+ {formatNumber(metric.latest_update.value)}
+
+
+ {metric.target_value && (
+
+
+
+ Target: {formatNumber(metric.target_value)}
+
+ = 100
+ ? "text-green-600"
+ : progress >= 50
+ ? "text-yellow-600"
+ : "text-red-600"
+ }`}
+ >
+ {progress.toFixed(1)}%
+
+
+
+
= 100
+ ? "bg-green-500"
+ : progress >= 50
+ ? "bg-yellow-500"
+ : "bg-red-500"
+ }`}
+ style={{ width: `${Math.min(progress, 100)}%` }}
+ />
+
+
+ )}
+
+ ) : (
+
+
+
+ No data submitted yet
+
+
+ )}
+
+ {/* Update Metric */}
+ {!isSelected ? (
+
setSelectedMetric(metric.id)}
+ className="mb-4 w-full rounded-lg bg-purple-500 px-4 py-2 text-white hover:bg-purple-600"
+ >
+ {metric.latest_update ? "Update Value" : "Submit Value"}
+
+ ) : (
+
+
setMetricValue(e.target.value)}
+ placeholder="Enter metric value"
+ className="w-full rounded-lg border border-gray-300 px-4 py-2 focus:border-purple-500 focus:outline-none"
+ />
+
+ {
+ setSelectedMetric(null);
+ setMetricValue("");
+ }}
+ className="flex-1 rounded-lg border border-gray-300 bg-white px-4 py-2 text-gray-700 hover:bg-gray-50"
+ >
+ Cancel
+
+ handleSubmitMetric(metric.id)}
+ disabled={submitting}
+ className="flex-1 rounded-lg bg-purple-500 px-4 py-2 text-white hover:bg-purple-600 disabled:opacity-50"
+ >
+ {submitting ? "Submitting..." : "Submit"}
+
+
+
+ )}
+
+ {/* Comments Section */}
+
+
+
+ Comments & Feedback
+
+
+ {/* Brand Feedback */}
+ {metric.feedback && metric.feedback.length > 0 && (
+
+ {metric.feedback.map((fb: any) => (
+
+
+
+ Brand Feedback
+
+
+ {new Date(fb.created_at).toLocaleDateString()}
+
+
+
{fb.feedback_text}
+
+ ))}
+
+ )}
+
+ {/* Creator Comments */}
+ {creatorComments.length > 0 && (
+
+ {creatorComments.map((comment: any, idx: number) => (
+
+
+
+ Your Comment
+
+
+ {new Date(comment.created_at).toLocaleDateString()}
+
+
+
{comment.text}
+
+ ))}
+
+ )}
+
+ {/* Add Comment */}
+
+ setCommentText(e.target.value)}
+ placeholder="Add a comment..."
+ className="flex-1 rounded-lg border border-gray-300 px-4 py-2 focus:border-purple-500 focus:outline-none"
+ onKeyPress={(e) => {
+ if (e.key === "Enter" && !submitting) {
+ handleSubmitComment(metric.id);
+ }
+ }}
+ />
+ handleSubmitComment(metric.id)}
+ disabled={submitting || !commentText.trim()}
+ className="rounded-lg bg-purple-500 px-4 py-2 text-white hover:bg-purple-600 disabled:opacity-50"
+ >
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+ );
+}
+
diff --git a/frontend/components/analytics/PlatformDeliverablesModal.tsx b/frontend/components/analytics/PlatformDeliverablesModal.tsx
new file mode 100644
index 0000000..f50494d
--- /dev/null
+++ b/frontend/components/analytics/PlatformDeliverablesModal.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import { X, Package, CheckCircle2, Clock, AlertCircle } from "lucide-react";
+
+interface Deliverable {
+ id: string;
+ contract_deliverable_id: string;
+ campaign_deliverable_id?: string;
+ description: string;
+ status: string;
+ due_date?: string;
+ platform: string;
+ content_type?: string;
+ quantity: number;
+ guidance?: string;
+}
+
+interface PlatformDeliverablesModalProps {
+ platform: string;
+ deliverables: Deliverable[];
+ onClose: () => void;
+ onDeliverableClick: (deliverableId: string) => void;
+}
+
+export default function PlatformDeliverablesModal({
+ platform,
+ deliverables,
+ onClose,
+ onDeliverableClick,
+}: PlatformDeliverablesModalProps) {
+ const getStatusColor = (status: string) => {
+ switch (status?.toLowerCase()) {
+ case "completed":
+ return "bg-green-100 text-green-800 border-green-200";
+ case "pending":
+ return "bg-yellow-100 text-yellow-800 border-yellow-200";
+ case "in_progress":
+ return "bg-blue-100 text-blue-800 border-blue-200";
+ default:
+ return "bg-gray-100 text-gray-800 border-gray-200";
+ }
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status?.toLowerCase()) {
+ case "completed":
+ return
;
+ case "pending":
+ return
;
+ default:
+ return
;
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
{platform}
+
+ {deliverables.length} deliverable{deliverables.length !== 1 ? "s" : ""}
+
+
+
+
+
+
+
+ {/* Deliverables List */}
+
+ {deliverables.length === 0 ? (
+
+
+
No deliverables found for this platform.
+
+ ) : (
+
+ {deliverables.map((deliverable) => (
+
{
+ onDeliverableClick(deliverable.id);
+ onClose();
+ }}
+ className="cursor-pointer rounded-lg border-2 p-4 transition-all hover:border-purple-500 hover:shadow-md"
+ >
+
+
+
+ {getStatusIcon(deliverable.status)}
+
+
+ {deliverable.content_type || deliverable.description}
+
+
+ {deliverable.description}
+
+ {deliverable.guidance && (
+
+ {deliverable.guidance}
+
+ )}
+
+
+
+ {deliverable.quantity > 1 && (
+ Quantity: {deliverable.quantity}
+ )}
+ {deliverable.due_date && (
+
+ Due: {new Date(deliverable.due_date).toLocaleDateString()}
+
+ )}
+
+
+
+ {deliverable.status}
+
+
+
+ Click to view metrics →
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
+
diff --git a/frontend/components/auth/AuthGuard.tsx b/frontend/components/auth/AuthGuard.tsx
new file mode 100644
index 0000000..681dd32
--- /dev/null
+++ b/frontend/components/auth/AuthGuard.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import { getCurrentUser, getUserProfile } from "@/lib/auth-helpers";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+interface AuthGuardProps {
+ children: React.ReactNode;
+ requiredRole?: "Creator" | "Brand";
+}
+
+export default function AuthGuard({ children, requiredRole }: AuthGuardProps) {
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(true);
+ const [isAuthorized, setIsAuthorized] = useState(false);
+
+ useEffect(() => {
+ async function checkAuth() {
+ try {
+ // Check if user is authenticated
+ const user = await getCurrentUser();
+
+ if (!user) {
+ router.push("/login");
+ return;
+ }
+
+ // If a specific role is required, verify it
+ if (requiredRole) {
+ const profile = await getUserProfile();
+
+ if (!profile) {
+ router.push("/login");
+ return;
+ }
+
+ if (profile.role !== requiredRole) {
+ // Redirect to correct home page based on their actual role
+ const correctPath =
+ profile.role === "Creator" ? "/creator/home" : "/brand/home";
+ router.push(correctPath);
+ return;
+ }
+ }
+
+ setIsAuthorized(true);
+ } catch (error) {
+ console.error("Auth check failed:", error);
+ router.push("/login");
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ checkAuth();
+ }, [router, requiredRole]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!isAuthorized) {
+ return null;
+ }
+
+ return <>{children}>;
+}
\ No newline at end of file
diff --git a/frontend/components/contracts/ContractsWorkspace.tsx b/frontend/components/contracts/ContractsWorkspace.tsx
new file mode 100644
index 0000000..08edfa5
--- /dev/null
+++ b/frontend/components/contracts/ContractsWorkspace.tsx
@@ -0,0 +1,2170 @@
+"use client";
+
+import {
+ approveContractVersion,
+ approveDeliverablesList,
+ askContractQuestion,
+ createContractVersion,
+ createOrUpdateDeliverablesList,
+ explainContractClause,
+ fetchContractChatMessages,
+ fetchContractDeliverables,
+ fetchContractDetail,
+ fetchContractVersions,
+ fetchContracts,
+ generateContractTemplate,
+ postContractChatMessage,
+ requestContractStatusChange,
+ respondToStatusChangeRequest,
+ reviewDeliverable,
+ submitDeliverable,
+ summarizeContract,
+ trackSignedContractDownload,
+ trackUnsignedContractDownload,
+ translateContract,
+ updateSignedContractLink,
+ updateUnsignedContractLink,
+ type ClauseExplanation,
+ type ContractQuestionAnswer,
+ type ContractSummary,
+ type ContractTemplate,
+ type ContractTranslation,
+} from "@/lib/api/proposals";
+import {
+ Contract,
+ ContractChatMessage,
+ ContractVersion,
+ ContractVersionCreate,
+ Deliverable,
+ DeliverableCreate,
+} from "@/types/proposals";
+import {
+ Check,
+ CheckCircle,
+ Download,
+ FileText,
+ HelpCircle,
+ History,
+ Languages,
+ Loader2,
+ MessageCircle,
+ Plus,
+ Send,
+ Sparkles,
+ Upload,
+ X,
+ XCircle,
+ FileCode,
+ BookOpen,
+} from "lucide-react";
+import { useEffect, useMemo, useRef, useState } from "react";
+
+interface ContractsWorkspaceProps {
+ role: "Brand" | "Creator";
+}
+
+function formatDate(dateString: string) {
+ try {
+ return new Date(dateString).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ });
+ } catch {
+ return dateString;
+ }
+}
+
+function formatDateTime(dateString: string) {
+ try {
+ return new Date(dateString).toLocaleString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ });
+ } catch {
+ return dateString;
+ }
+}
+
+function prettyPrintJson(obj: any): string {
+ try {
+ return JSON.stringify(obj, null, 2);
+ } catch {
+ return String(obj);
+ }
+}
+
+function formatStatus(status: string): string {
+ return status
+ .split("_")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(" ");
+}
+
+function isWorkflowComplete(contract: Contract): boolean {
+ return !!(
+ contract.unsigned_contract_link &&
+ contract.unsigned_contract_downloaded_by_creator &&
+ contract.signed_contract_link &&
+ contract.signed_contract_downloaded_by_brand
+ );
+}
+
+function getThreadIcon(type: string) {
+ switch (type) {
+ case "initial_message":
+ return
;
+ case "terms_update":
+ return
;
+ default:
+ return
;
+ }
+}
+
+export function ContractsWorkspace({ role }: ContractsWorkspaceProps) {
+ const [contracts, setContracts] = useState
([]);
+ const [selectedContractId, setSelectedContractId] = useState(null);
+ const [contractDetail, setContractDetail] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [globalError, setGlobalError] = useState(null);
+ const [globalSuccess, setGlobalSuccess] = useState(null);
+ const [unsignedLinkInput, setUnsignedLinkInput] = useState("");
+ const [signedLinkInput, setSignedLinkInput] = useState("");
+ const [linkSubmitting, setLinkSubmitting] = useState(false);
+ const [statusChangeSubmitting, setStatusChangeSubmitting] = useState(false);
+ const [selectedNewStatus, setSelectedNewStatus] = useState("");
+ const [contractChatMessages, setContractChatMessages] = useState([]);
+ const [chatMessageDraft, setChatMessageDraft] = useState("");
+ const [chatSubmitting, setChatSubmitting] = useState(false);
+ const [chatLoading, setChatLoading] = useState(false);
+ const chatMessagesEndRef = useRef(null);
+ const [deliverables, setDeliverables] = useState([]);
+ const [deliverablesLoading, setDeliverablesLoading] = useState(false);
+ const [editingDeliverables, setEditingDeliverables] = useState(false);
+ const [deliverableDrafts, setDeliverableDrafts] = useState([]);
+ const [submissionUrl, setSubmissionUrl] = useState>({});
+ const [reviewComment, setReviewComment] = useState>({});
+ const [rejectionReason, setRejectionReason] = useState>({});
+ const [submittingDeliverable, setSubmittingDeliverable] = useState(null);
+ const [reviewingDeliverable, setReviewingDeliverable] = useState(null);
+ const [contractVersions, setContractVersions] = useState([]);
+ const [currentVersion, setCurrentVersion] = useState(null);
+ const [versionsLoading, setVersionsLoading] = useState(false);
+ const [showAmendmentModal, setShowAmendmentModal] = useState(false);
+ const [amendmentFileUrl, setAmendmentFileUrl] = useState("");
+ const [amendmentReason, setAmendmentReason] = useState("");
+ const [creatingAmendment, setCreatingAmendment] = useState(false);
+ const [approvingVersion, setApprovingVersion] = useState(null);
+
+ // AI Features State
+ const [questionText, setQuestionText] = useState("");
+ const [questionAnswer, setQuestionAnswer] = useState(null);
+ const [loadingQuestion, setLoadingQuestion] = useState(false);
+ const [contractSummary, setContractSummary] = useState(null);
+ const [loadingSummary, setLoadingSummary] = useState(false);
+ const [clauseText, setClauseText] = useState("");
+ const [clauseContext, setClauseContext] = useState("");
+ const [clauseExplanation, setClauseExplanation] = useState(null);
+ const [loadingClause, setLoadingClause] = useState(false);
+ const [translationLanguage, setTranslationLanguage] = useState("es");
+ const [contractTranslation, setContractTranslation] = useState(null);
+ const [loadingTranslation, setLoadingTranslation] = useState(false);
+ const [showTemplateModal, setShowTemplateModal] = useState(false);
+ const [templateData, setTemplateData] = useState({
+ deal_type: "",
+ deliverables: "",
+ payment_amount: "",
+ duration: "",
+ additional_requirements: "",
+ });
+ const [generatedTemplate, setGeneratedTemplate] = useState