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?

+
+ + +
+
+ ); + + // 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

+
+ { + // Only allow letters, spaces, and basic punctuation + const value = e.target.value.replace(/[^a-zA-Z\s.'-]/g, ""); + setPersonal({ ...personal, name: value }); + }} + className="px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500" + required + /> + setPersonal({ ...personal, email: e.target.value })} + className="px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500" + disabled + /> + { + // Only allow numbers and limit to 2 digits + let value = e.target.value.replace(/[^0-9]/g, ""); + if (value.length > 2) value = value.slice(0, 2); + setPersonal({ ...personal, age: value }); + }} + className="px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500" + min={10} + max={99} + required + /> + + + {personal.category === "Other" && ( + setPersonal({ ...personal, otherCategory: e.target.value })} + className="px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500" + required + /> + )} + { + // Only allow letters and spaces + const value = e.target.value.replace(/[^a-zA-Z\s]/g, ""); + setPersonal({ ...personal, country: value }); + }} + className="px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-purple-500" + required + /> +
+ {personalError &&
{personalError}
} +
+ ); -
- {/* Influencer Button */} + // Step 3: Platform Selection + const renderPlatformStep = () => ( +
+

Which platforms do you use?

+
+ {platforms.map((platform) => ( + ))} +
+
+ ); + + // 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

+
+ + +
+ {(profilePic || user?.user_metadata?.avatar_url) ? ( + Profile Preview + ) : ( +
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 && ( +
+
+
+ )} +
+ +
+ {(profilePic || user?.user_metadata?.avatar_url) ? ( + Profile Preview + ) : ( +
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 Details

+ setBrandData({ ...brandData, brand_name: e.target.value })} + className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2" + /> + + + + {brandLogoPreview && Logo Preview} + setBrandData({ ...brandData, website_url: e.target.value })} + className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2" + /> + + + setBrandData({ ...brandData, location: e.target.value })} + className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2" + /> +