diff --git a/Backend/.env-example b/Backend/.env-example
index f23fbd0..18e42cd 100644
--- a/Backend/.env-example
+++ b/Backend/.env-example
@@ -5,4 +5,6 @@ port=5432
dbname=postgres
GROQ_API_KEY=
SUPABASE_URL=
-SUPABASE_KEY=
\ No newline at end of file
+SUPABASE_KEY=
+GEMINI_API_KEY=
+YOUTUBE_API_KEY=
\ No newline at end of file
diff --git a/Backend/app/db/seed.py b/Backend/app/db/seed.py
index 9b8936a..77a015e 100644
--- a/Backend/app/db/seed.py
+++ b/Backend/app/db/seed.py
@@ -1,6 +1,6 @@
-from datetime import datetime, timezone
-from db.db import AsyncSessionLocal
-from models.models import User
+from datetime import datetime
+from app.db.db import AsyncSessionLocal
+from app.models.models import User
async def seed_db():
@@ -12,6 +12,8 @@ async def seed_db():
"password": "password123",
"role": "creator",
"bio": "Lifestyle and travel content creator",
+ "profile_image": None,
+ "created_at": datetime.utcnow()
},
{
"id": "6dbfcdd5-795f-49c1-8f7a-a5538b8c6f6f",
@@ -20,6 +22,8 @@ async def seed_db():
"password": "password123",
"role": "brand",
"bio": "Sustainable fashion brand looking for influencers",
+ "profile_image": None,
+ "created_at": datetime.utcnow()
},
]
@@ -40,11 +44,10 @@ async def seed_db():
id=user_data["id"],
username=user_data["username"],
email=user_data["email"],
- password_hash=user_data[
- "password"
- ], # Using plain password directly
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']}")
diff --git a/Backend/app/main.py b/Backend/app/main.py
index 41ef7b7..86d892a 100644
--- a/Backend/app/main.py
+++ b/Backend/app/main.py
@@ -1,15 +1,17 @@
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 .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()
@@ -51,6 +53,9 @@ async def lifespan(app: FastAPI):
# 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("/")
diff --git a/Backend/app/models/chat.py b/Backend/app/models/chat.py
index 5727cbf..16c6d93 100644
--- a/Backend/app/models/chat.py
+++ b/Backend/app/models/chat.py
@@ -1,7 +1,7 @@
from sqlalchemy import Column, String, ForeignKey, DateTime, Enum, UniqueConstraint
from sqlalchemy.orm import relationship
from datetime import datetime, timezone
-from db.db import Base
+from app.db.db import Base
import uuid
import enum
diff --git a/Backend/app/models/models.py b/Backend/app/models/models.py
index fec8452..56681ab 100644
--- a/Backend/app/models/models.py
+++ b/Backend/app/models/models.py
@@ -9,10 +9,11 @@
DECIMAL,
DateTime,
Boolean,
+ TIMESTAMP,
)
from sqlalchemy.orm import relationship
-from datetime import datetime, timezone
-from db.db import Base
+from datetime import datetime
+from app.db.db import Base
import uuid
@@ -27,18 +28,14 @@ class User(Base):
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) # Restored for now
+ # 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(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
+ created_at = Column(TIMESTAMP, default=datetime.utcnow)
is_online = Column(Boolean, default=False) # ✅ Track if user is online
- last_seen = Column(
- DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
- )
+ last_seen = Column(TIMESTAMP, default=datetime.utcnow)
audience = relationship("AudienceInsights", back_populates="user", uselist=False)
sponsorships = relationship("Sponsorship", back_populates="brand")
diff --git a/Backend/app/routes/ai.py b/Backend/app/routes/ai.py
index e69de29..a21a482 100644
--- a/Backend/app/routes/ai.py
+++ b/Backend/app/routes/ai.py
@@ -0,0 +1,101 @@
+# 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
index e69de29..19d59a2 100644
--- a/Backend/app/routes/auth.py
+++ b/Backend/app/routes/auth.py
@@ -0,0 +1,7 @@
+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
index 1188d32..f51d6b7 100644
--- a/Backend/app/routes/chat.py
+++ b/Backend/app/routes/chat.py
@@ -7,12 +7,12 @@
HTTPException,
)
from sqlalchemy.ext.asyncio import AsyncSession
-from db.db import get_db
-from services.chat_services import chat_service
+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
+from ..services.redis_client import get_redis
import asyncio
-from services.chat_pubsub import listen_to_channel
+from ..services.chat_pubsub import listen_to_channel
router = APIRouter(prefix="/chat", tags=["Chat"])
diff --git a/Backend/app/routes/match.py b/Backend/app/routes/match.py
new file mode 100644
index 0000000..48ba7f5
--- /dev/null
+++ b/Backend/app/routes/match.py
@@ -0,0 +1,29 @@
+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
index d0f1413..a90e313 100644
--- a/Backend/app/routes/post.py
+++ b/Backend/app/routes/post.py
@@ -1,12 +1,12 @@
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 (
+from ..db.db import AsyncSessionLocal
+from ..models.models import (
User, AudienceInsights, Sponsorship, UserPost,
SponsorshipApplication, SponsorshipPayment, Collaboration
)
-from schemas.schema import (
+from ..schemas.schema import (
UserCreate, AudienceInsightsCreate, SponsorshipCreate, UserPostCreate,
SponsorshipApplicationCreate, SponsorshipPaymentCreate, CollaborationCreate
)
@@ -44,7 +44,6 @@ async def create_user(user: UserCreate):
"id": user_id,
"username": user.username,
"email": user.email,
- "password_hash": user.password_hash,
"role": user.role,
"profile_image": user.profile_image,
"bio": user.bio,
diff --git a/Backend/app/schemas/schema.py b/Backend/app/schemas/schema.py
index 3bd1e8c..7389488 100644
--- a/Backend/app/schemas/schema.py
+++ b/Backend/app/schemas/schema.py
@@ -5,7 +5,6 @@
class UserCreate(BaseModel):
username: str
email: str
- password_hash: str
role: str
profile_image: Optional[str] = None
bio: Optional[str] = None
diff --git a/Backend/app/services/chat_services.py b/Backend/app/services/chat_services.py
index 7210990..4b5d1a6 100644
--- a/Backend/app/services/chat_services.py
+++ b/Backend/app/services/chat_services.py
@@ -2,8 +2,8 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql import select
from datetime import datetime, timezone
-from models.models import User
-from models.chat import ChatList, ChatMessage, MessageStatus
+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
diff --git a/Backend/app/services/db_service.py b/Backend/app/services/db_service.py
index e69de29..ccb4199 100644
--- a/Backend/app/services/db_service.py
+++ b/Backend/app/services/db_service.py
@@ -0,0 +1,85 @@
+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/sql.txt b/Backend/sql.txt
index 1f641c9..3ee28b5 100644
--- a/Backend/sql.txt
+++ b/Backend/sql.txt
@@ -1,8 +1,8 @@
-- Insert into users table
-INSERT INTO users (id, username, email, password_hash, role, profile_image, bio, created_at) VALUES
- (gen_random_uuid(), 'creator1', 'creator1@example.com', 'hashedpassword1', 'creator', 'image1.jpg', 'Bio of creator1', NOW()),
- (gen_random_uuid(), 'brand1', 'brand1@example.com', 'hashedpassword2', 'brand', 'image2.jpg', 'Bio of brand1', NOW()),
- (gen_random_uuid(), 'creator2', 'creator2@example.com', 'hashedpassword3', 'creator', 'image3.jpg', 'Bio of creator2', NOW());
+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
diff --git a/Frontend/env-example b/Frontend/env-example
index 407c780..4ce57da 100644
--- a/Frontend/env-example
+++ b/Frontend/env-example
@@ -1,2 +1,3 @@
VITE_SUPABASE_URL=https://your-project.supabase.co
-VITE_SUPABASE_ANON_KEY=your-anon-key-here
\ No newline at end of file
+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/public/brand.png b/Frontend/public/brand.png
new file mode 100644
index 0000000..bc29669
Binary files /dev/null and b/Frontend/public/brand.png differ
diff --git a/Frontend/public/contnetcreator.png b/Frontend/public/contnetcreator.png
new file mode 100644
index 0000000..e527063
Binary files /dev/null and b/Frontend/public/contnetcreator.png differ
diff --git a/Frontend/public/facebook.png b/Frontend/public/facebook.png
new file mode 100644
index 0000000..0c37594
Binary files /dev/null and b/Frontend/public/facebook.png differ
diff --git a/Frontend/public/instagram.png b/Frontend/public/instagram.png
new file mode 100644
index 0000000..82216ba
Binary files /dev/null and b/Frontend/public/instagram.png differ
diff --git a/Frontend/public/tiktok.png b/Frontend/public/tiktok.png
new file mode 100644
index 0000000..4b6a8ae
Binary files /dev/null and b/Frontend/public/tiktok.png differ
diff --git a/Frontend/public/youtube.png b/Frontend/public/youtube.png
new file mode 100644
index 0000000..2db89d2
Binary files /dev/null and b/Frontend/public/youtube.png differ
diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx
index 15b8bc6..be41d2e 100644
--- a/Frontend/src/App.tsx
+++ b/Frontend/src/App.tsx
@@ -1,4 +1,5 @@
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";
@@ -10,26 +11,69 @@ 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*/}
;
+ 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("");
- return (
-
-
-
Welcome to Inpact
-
- Let's get you started with your profile setup
-
+ // 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}
}
+
+ );
-
- {/* Influencer Button */}
+ // Step 3: Platform Selection
+ const renderPlatformStep = () => (
+
+
Which platforms do you use?
+
+ {platforms.map((platform) => (
navigate("/signup")}
- className="w-full flex items-center justify-between bg-purple-600 text-white font-medium px-6 py-3 rounded-lg shadow-md hover:bg-purple-700 transition"
+ key={platform.name}
+ type="button"
+ onClick={() => {
+ 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"}`}
>
- I'm an Influencer
-
+
+ {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);
+ }
+ };
- {/* Brand Button */}
+ 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 => (
navigate("/signup")}
- className="w-full flex items-center justify-between bg-purple-600 text-white font-medium px-6 py-3 rounded-lg shadow-md hover:bg-purple-700 transition"
+ key={platform.name}
+ type="button"
+ onClick={() => {
+ 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"}`}
>
- I'm a Brand
-
+ {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
index b627619..5f9564f 100644
--- a/Frontend/src/components/ProtectedRoute.tsx
+++ b/Frontend/src/components/ProtectedRoute.tsx
@@ -2,9 +2,9 @@ import { Navigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
- const { user } = useAuth();
+ const { isAuthenticated } = useAuth();
- return user ? children : ;
+ return isAuthenticated ? children : ;
};
export default ProtectedRoute;
diff --git a/Frontend/src/components/PublicRoute.tsx b/Frontend/src/components/PublicRoute.tsx
new file mode 100644
index 0000000..7bb29a7
--- /dev/null
+++ b/Frontend/src/components/PublicRoute.tsx
@@ -0,0 +1,69 @@
+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/dashboard/creator-matches.tsx b/Frontend/src/components/dashboard/creator-matches.tsx
new file mode 100644
index 0000000..2252819
--- /dev/null
+++ b/Frontend/src/components/dashboard/creator-matches.tsx
@@ -0,0 +1,85 @@
+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/sponsorship-matches.tsx b/Frontend/src/components/dashboard/sponsorship-matches.tsx
index 94014e6..daf6ee8 100644
--- a/Frontend/src/components/dashboard/sponsorship-matches.tsx
+++ b/Frontend/src/components/dashboard/sponsorship-matches.tsx
@@ -1,99 +1,83 @@
+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"
-export function SponsorshipMatches() {
+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 (
-
-
-
-
-
- ES
-
-
-
-
EcoStyle
- 98% Match
-
-
Sustainable fashion brand looking for lifestyle creators
-
- $3,000 - $5,000
- •
- 1 post, 2 stories
-
-
- View Details
-
- Contact
-
-
-
-
-
-
-
-
-
-
-
- TG
-
-
-
-
TechGadgets
- 95% Match
-
-
- Tech company seeking reviewers for new smart home products
-
-
- $2,500 - $4,000
- •
- Review video + social posts
-
-
- View Details
-
- Contact
-
-
-
-
-
-
-
-
-
-
-
- FL
-
-
-
-
FitLife Supplements
- 92% Match
-
-
- Fitness supplement brand looking for health & wellness creators
-
-
- $1,800 - $3,500
- •
- 3-month campaign
-
-
-
View Details
-
- Contact
-
+ {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/main-nav.tsx b/Frontend/src/components/main-nav.tsx
index 2e58eb5..9aa97aa 100644
--- a/Frontend/src/components/main-nav.tsx
+++ b/Frontend/src/components/main-nav.tsx
@@ -3,18 +3,9 @@ import { Link } from "react-router-dom"
export function MainNav() {
return (
-
- Features
-
-
- Pricing
-
-
- About
-
-
- Contact
-
+ {/* Navigation items removed - keeping component for future use */}
+ {/* TODO: Under construction - menu items coming soon */}
+ Menu coming soon
)
}
diff --git a/Frontend/src/components/user-nav.tsx b/Frontend/src/components/user-nav.tsx
index 2d20d51..9c4939f 100644
--- a/Frontend/src/components/user-nav.tsx
+++ b/Frontend/src/components/user-nav.tsx
@@ -1,5 +1,4 @@
import React from "react";
-
import { useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { Button } from "./ui/button";
@@ -12,47 +11,63 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
+import { useAuth } from "../context/AuthContext";
+import { Link } from "react-router-dom";
export function UserNav() {
- const [isLoggedIn, setIsLoggedIn] = useState(false);
+ const { user, isAuthenticated, logout } = useAuth();
+ const [avatarError, setAvatarError] = useState(false);
- if (!isLoggedIn) {
+ if (!isAuthenticated || !user) {
return (
-
-
- U
-
-
+
+
+ Login
+
+
+ Sign Up
+
+
);
}
+ const handleAvatarError = () => {
+ setAvatarError(true);
+ };
+
return (
-
- U
+
+ {user.user_metadata?.name?.charAt(0) || user.email?.charAt(0) || "U"}
-
User Name
+
{user.user_metadata?.name || "User"}
- user@example.com
+ {user.email}
+
+ Dashboard
+
Profile
Settings
- Billing
- Log out
+ Log out
);
diff --git a/Frontend/src/context/AuthContext.tsx b/Frontend/src/context/AuthContext.tsx
index 53c9507..8588c41 100644
--- a/Frontend/src/context/AuthContext.tsx
+++ b/Frontend/src/context/AuthContext.tsx
@@ -5,7 +5,7 @@ import {
ReactNode,
useEffect,
} from "react";
-import { useNavigate, useLocation } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
import { supabase, User } from "../utils/supabase";
interface AuthContextType {
@@ -13,6 +13,7 @@ interface AuthContextType {
user: User | null;
login: () => void;
logout: () => void;
+ checkUserOnboarding: (userToCheck?: User | null) => Promise<{ hasOnboarding: boolean; role: string | null }>;
}
const AuthContext = createContext
(undefined);
@@ -21,34 +22,163 @@ 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();
- const location = useLocation();
+
+ // 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(() => {
- supabase.auth.getSession().then(({ data }) => {
+ 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(
- (event, session) => {
+ async (event, session) => {
+ if (!mounted) return;
+
+ console.log("AuthContext: Auth state change", { event, user: session?.user?.email });
+
setUser(session?.user || null);
- // Only redirect to dashboard if not on /reset-password and not during password recovery
+ setIsAuthenticated(!!session?.user);
- if (
- session?.user &&
- location.pathname !== "/reset-password" &&
- event !== "PASSWORD_RECOVERY"
- ) {
- navigate("/dashboard");
+ 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 () => listener.subscription.unsubscribe();
- }, [location.pathname, navigate]);
+ return () => {
+ mounted = false;
+ clearTimeout(timeoutId);
+ listener.subscription.unsubscribe();
+ };
+ }, []);
const login = () => {
setIsAuthenticated(true);
@@ -59,11 +189,26 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
await supabase.auth.signOut();
setUser(null);
setIsAuthenticated(false);
- navigate("/login");
+ 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}
);
@@ -74,4 +219,4 @@ export const useAuth = () => {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
-};
+};
\ No newline at end of file
diff --git a/Frontend/src/index.css b/Frontend/src/index.css
index 56b2e5a..f2a93bb 100644
--- a/Frontend/src/index.css
+++ b/Frontend/src/index.css
@@ -118,3 +118,64 @@
@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/pages/BasicDetails.tsx b/Frontend/src/pages/BasicDetails.tsx
index a1fa5f9..d72e0ef 100644
--- a/Frontend/src/pages/BasicDetails.tsx
+++ b/Frontend/src/pages/BasicDetails.tsx
@@ -30,7 +30,6 @@ import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { UserNav } from "../components/user-nav";
-import { MainNav } from "../components/main-nav";
import { Link } from "react-router-dom";
import { ModeToggle } from "../components/mode-toggle";
diff --git a/Frontend/src/pages/Brand/Dashboard.tsx b/Frontend/src/pages/Brand/Dashboard.tsx
index d2b4dcd..023c77b 100644
--- a/Frontend/src/pages/Brand/Dashboard.tsx
+++ b/Frontend/src/pages/Brand/Dashboard.tsx
@@ -33,8 +33,18 @@ import {
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 (
<>
@@ -159,46 +169,24 @@ const Dashboard = () => {
- Recommended Creators
+ Matched Creators for Your Campaign
- View All
-
- {[1, 2, 3].map((i) => (
-
-
-
-
-
-
- Sarah Parker
-
-
- Lifestyle & Fashion
-
-
-
-
-
- View Profile
-
-
-
- ))}
+
+ Select Campaign:
+
+
diff --git a/Frontend/src/pages/DashboardPage.tsx b/Frontend/src/pages/DashboardPage.tsx
index fa2ea4f..fba9c86 100644
--- a/Frontend/src/pages/DashboardPage.tsx
+++ b/Frontend/src/pages/DashboardPage.tsx
@@ -24,7 +24,7 @@ import { SponsorshipMatches } from "../components/dashboard/sponsorship-matches"
import { useAuth } from "../context/AuthContext"
export default function DashboardPage() {
- const {logout} = useAuth();
+ const {logout, user} = useAuth();
return (
@@ -180,7 +180,7 @@ export default function DashboardPage() {
Brands that match your audience and content
-
+
diff --git a/Frontend/src/pages/HomePage.tsx b/Frontend/src/pages/HomePage.tsx
index b8d95a9..011641d 100644
--- a/Frontend/src/pages/HomePage.tsx
+++ b/Frontend/src/pages/HomePage.tsx
@@ -8,107 +8,703 @@ import {
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 Onboarding from "../components/Onboarding";
+import { useAuth } from "../context/AuthContext";
+import { supabase } from "../utils/supabase";
const features = [
{
icon: Handshake,
- title: "AI-Driven Sponsorship Matchmaking",
+ 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: "AI-Based Pricing Optimization",
+ 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: "Negotiation & Contract Assistant",
+ 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
+ // 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);
- // Set up intersection observer for scroll detection
+ // 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 featuresObserver = new IntersectionObserver(
+ 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;
- setIsFeaturesVisible(entry.isIntersecting);
+ if (entry.isIntersecting && !hasAnimatedBrands) {
+ setIsSuccessStoriesVisible(true);
+ setHasAnimatedBrands(true);
+ }
},
- {
- root: null,
- rootMargin: "0px",
- threshold: 0.1, // Trigger when 10% of the element is visible
- }
+ { 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,
- }
+ { root: null, rootMargin: "0px", threshold: 0.1 }
);
-
- if (featuresRef.current) {
- featuresObserver.observe(featuresRef.current);
- }
-
- if (footerRef.current) {
- footerObserver.observe(footerRef.current);
- }
-
+ if (footerRef.current) footerObserver.observe(footerRef.current);
return () => {
- if (featuresRef.current) {
- featuresObserver.unobserve(featuresRef.current);
- }
- if (footerRef.current) {
- footerObserver.unobserve(footerRef.current);
- }
+ 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 (
-