diff --git a/backend/app/api/routes/profiles.py b/backend/app/api/routes/profiles.py
new file mode 100644
index 0000000..3aa4142
--- /dev/null
+++ b/backend/app/api/routes/profiles.py
@@ -0,0 +1,633 @@
+"""
+Profile management routes for brands and creators
+"""
+
+import httpx
+import json
+from fastapi import APIRouter, HTTPException, Depends
+from pydantic import BaseModel
+from typing import Optional, Dict, Any
+from app.core.supabase_clients import supabase_anon
+from app.core.dependencies import get_current_user, get_current_brand, get_current_creator
+from app.core.config import settings
+
+router = APIRouter()
+GEMINI_API_KEY = settings.gemini_api_key
+GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
+
+
+class ProfileUpdateRequest(BaseModel):
+ """Generic profile update request - accepts any fields"""
+ data: Dict[str, Any]
+
+
+def calculate_brand_completion_percentage(brand: dict) -> int:
+ """Calculate profile completion percentage for a brand"""
+ required_fields = [
+ 'company_name', 'industry', 'website_url', 'company_description',
+ 'company_logo_url', 'contact_email', 'contact_phone'
+ ]
+
+ important_fields = [
+ 'company_tagline', 'headquarters_location', 'company_size',
+ 'target_audience_description', 'brand_values', 'brand_personality',
+ 'marketing_goals', 'preferred_platforms', 'monthly_marketing_budget'
+ ]
+
+ nice_to_have_fields = [
+ 'company_cover_image_url', 'social_media_links', 'founded_year',
+ 'brand_voice', 'campaign_types_interested', 'preferred_content_types'
+ ]
+
+ completed = 0
+ total = len(required_fields) + len(important_fields) + len(nice_to_have_fields)
+
+ # Required fields (weight: 3x)
+ for field in required_fields:
+ if brand.get(field):
+ completed += 3
+
+ # Important fields (weight: 2x)
+ for field in important_fields:
+ if brand.get(field):
+ completed += 2
+
+ # Nice to have fields (weight: 1x)
+ for field in nice_to_have_fields:
+ if brand.get(field):
+ completed += 1
+
+ # Calculate percentage
+ max_score = len(required_fields) * 3 + len(important_fields) * 2 + len(nice_to_have_fields)
+ percentage = int((completed / max_score) * 100) if max_score > 0 else 0
+ return min(100, max(0, percentage))
+
+
+def calculate_creator_completion_percentage(creator: dict) -> int:
+ """Calculate profile completion percentage for a creator"""
+ required_fields = [
+ 'display_name', 'primary_niche', 'profile_picture_url', 'bio'
+ ]
+
+ important_fields = [
+ 'tagline', 'website_url', 'instagram_handle', 'youtube_handle',
+ 'content_types', 'collaboration_types', 'rate_per_post',
+ 'years_of_experience', 'posting_frequency'
+ ]
+
+ nice_to_have_fields = [
+ 'cover_image_url', 'secondary_niches', 'content_language',
+ 'portfolio_links', 'media_kit_url', 'equipment_quality',
+ 'editing_software', 'preferred_payment_terms'
+ ]
+
+ completed = 0
+ total = len(required_fields) + len(important_fields) + len(nice_to_have_fields)
+
+ # Required fields (weight: 3x)
+ for field in required_fields:
+ if creator.get(field):
+ completed += 3
+
+ # Important fields (weight: 2x)
+ for field in important_fields:
+ if creator.get(field):
+ completed += 2
+
+ # Nice to have fields (weight: 1x)
+ for field in nice_to_have_fields:
+ if creator.get(field):
+ completed += 1
+
+ # Calculate percentage
+ max_score = len(required_fields) * 3 + len(important_fields) * 2 + len(nice_to_have_fields)
+ percentage = int((completed / max_score) * 100) if max_score > 0 else 0
+ return min(100, max(0, percentage))
+
+
+@router.get("/brand/profile")
+async def get_brand_profile(
+ brand: dict = Depends(get_current_brand)
+):
+ """Get the current brand's profile"""
+ try:
+ # Calculate completion percentage
+ completion = calculate_brand_completion_percentage(brand)
+
+ # Update completion percentage in database if different
+ if brand.get('profile_completion_percentage') != completion:
+ supabase_anon.table('brands') \
+ .update({'profile_completion_percentage': completion}) \
+ .eq('id', brand['id']) \
+ .execute()
+ brand['profile_completion_percentage'] = completion
+
+ return brand
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching brand profile: {str(e)}"
+ ) from e
+
+
+@router.put("/brand/profile")
+async def update_brand_profile(
+ update_request: ProfileUpdateRequest,
+ brand: dict = Depends(get_current_brand)
+):
+ """Update the current brand's profile"""
+ try:
+ update_data = update_request.data
+
+ # Remove fields that shouldn't be updated directly
+ restricted_fields = ['id', 'user_id', 'created_at', 'is_active']
+ for field in restricted_fields:
+ update_data.pop(field, None)
+
+ # Add updated_at timestamp
+ from datetime import datetime
+ update_data['updated_at'] = datetime.utcnow().isoformat()
+
+ # Update in database
+ response = supabase_anon.table('brands') \
+ .update(update_data) \
+ .eq('id', brand['id']) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(
+ status_code=404,
+ detail="Brand profile not found"
+ )
+
+ updated_brand = response.data[0] if response.data else brand
+
+ # Recalculate completion percentage
+ completion = calculate_brand_completion_percentage(updated_brand)
+
+ # Update completion percentage
+ if updated_brand.get('profile_completion_percentage') != completion:
+ supabase_anon.table('brands') \
+ .update({'profile_completion_percentage': completion}) \
+ .eq('id', brand['id']) \
+ .execute()
+ updated_brand['profile_completion_percentage'] = completion
+
+ return updated_brand
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error updating brand profile: {str(e)}"
+ ) from e
+
+
+@router.get("/creator/profile")
+async def get_creator_profile(
+ creator: dict = Depends(get_current_creator)
+):
+ """Get the current creator's profile"""
+ try:
+ # Calculate completion percentage
+ completion = calculate_creator_completion_percentage(creator)
+
+ # Update completion percentage in database if different
+ if creator.get('profile_completion_percentage') != completion:
+ supabase_anon.table('creators') \
+ .update({'profile_completion_percentage': completion}) \
+ .eq('id', creator['id']) \
+ .execute()
+ creator['profile_completion_percentage'] = completion
+
+ return creator
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error fetching creator profile: {str(e)}"
+ ) from e
+
+
+@router.put("/creator/profile")
+async def update_creator_profile(
+ update_request: ProfileUpdateRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """Update the current creator's profile"""
+ try:
+ update_data = update_request.data
+
+ # Remove fields that shouldn't be updated directly
+ restricted_fields = ['id', 'user_id', 'created_at', 'is_active']
+ for field in restricted_fields:
+ update_data.pop(field, None)
+
+ # Add updated_at timestamp
+ from datetime import datetime
+ update_data['updated_at'] = datetime.utcnow().isoformat()
+
+ # Update in database
+ response = supabase_anon.table('creators') \
+ .update(update_data) \
+ .eq('id', creator['id']) \
+ .execute()
+
+ if not response.data:
+ raise HTTPException(
+ status_code=404,
+ detail="Creator profile not found"
+ )
+
+ updated_creator = response.data[0] if response.data else creator
+
+ # Recalculate completion percentage
+ completion = calculate_creator_completion_percentage(updated_creator)
+
+ # Update completion percentage
+ if updated_creator.get('profile_completion_percentage') != completion:
+ supabase_anon.table('creators') \
+ .update({'profile_completion_percentage': completion}) \
+ .eq('id', creator['id']) \
+ .execute()
+ updated_creator['profile_completion_percentage'] = completion
+
+ return updated_creator
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error updating creator profile: {str(e)}"
+ ) from e
+
+
+class AIFillRequest(BaseModel):
+ """Request for AI profile filling"""
+ user_input: str
+ context: Optional[Dict[str, Any]] = None
+
+
+@router.post("/brand/profile/ai-fill")
+async def ai_fill_brand_profile(
+ request: AIFillRequest,
+ brand: dict = Depends(get_current_brand)
+):
+ """Use AI to fill brand profile based on user input"""
+ if not GEMINI_API_KEY:
+ raise HTTPException(
+ status_code=500,
+ detail="Gemini API is not configured"
+ )
+
+ try:
+ # Build prompt for Gemini
+ prompt = f"""You are an expert at extracting structured data from natural language. Your task is to analyze user input and extract ALL relevant brand profile information.
+
+Current brand profile (for context - only fill fields that are empty or null):
+{json.dumps(brand, indent=2, default=str)}
+
+User provided information:
+{request.user_input}
+
+Extract and return a JSON object with ALL fields that can be confidently determined from the user input. Use these exact field names and data types:
+
+STRING fields:
+- company_name, company_tagline, company_description, company_logo_url, company_cover_image_url
+- industry, company_size, headquarters_location, company_type, website_url
+- contact_email, contact_phone, campaign_frequency, payment_terms
+- target_audience_description, brand_voice, product_price_range, product_catalog_url
+
+NUMBER fields (use numbers, not strings):
+- founded_year (integer), monthly_marketing_budget (numeric), influencer_budget_percentage (float 0-100)
+- budget_per_campaign_min (numeric), budget_per_campaign_max (numeric), typical_deal_size (numeric)
+- affiliate_commission_rate (float 0-100), minimum_followers_required (integer)
+- minimum_engagement_rate (float 0-100), exclusivity_duration_months (integer)
+- past_campaigns_count (integer), average_campaign_roi (float)
+- total_deals_posted (integer), total_deals_completed (integer), total_spent (numeric)
+- average_deal_rating (float), matching_score_base (float)
+
+BOOLEAN fields (use true/false):
+- offers_product_only_deals, offers_affiliate_programs, exclusivity_required
+- seasonal_products, business_verified, payment_verified, tax_id_verified
+- is_active, is_featured, is_verified_brand
+
+ARRAY fields (use JSON arrays of strings):
+- sub_industry, target_audience_age_groups, target_audience_gender, target_audience_locations
+- target_audience_interests, target_audience_income_level, brand_values, brand_personality
+- marketing_goals, campaign_types_interested, preferred_content_types, preferred_platforms
+- preferred_creator_niches, preferred_creator_size, preferred_creator_locations
+- content_dos, content_donts, brand_safety_requirements, competitor_brands
+- successful_partnerships, products_services, product_categories, search_keywords
+
+JSON OBJECT fields (use proper JSON objects):
+- social_media_links: {{"platform": "url", ...}}
+- brand_colors: {{"primary": "#hex", "secondary": "#hex", ...}}
+
+IMPORTANT RULES:
+1. Extract ALL fields that can be inferred from the input, not just obvious ones
+2. For arrays, extract multiple values if mentioned (e.g., "tech and finance" → ["tech", "finance"])
+3. For numbers, extract numeric values (e.g., "$50,000" → 50000, "5%" → 5.0)
+4. For booleans, infer from context (e.g., "we offer affiliate programs" → true)
+5. Only include fields that have clear values - omit uncertain fields
+6. Return ONLY valid JSON, no markdown, no explanations
+
+Return the JSON object now:"""
+
+ payload = {
+ "contents": [
+ {
+ "role": "user",
+ "parts": [{"text": prompt}]
+ }
+ ],
+ "generationConfig": {
+ "temperature": 0.1,
+ "topK": 40,
+ "topP": 0.95,
+ "maxOutputTokens": 8192,
+ "responseMimeType": "application/json"
+ }
+ }
+ headers = {"Content-Type": "application/json"}
+ params = {"key": GEMINI_API_KEY}
+
+ async with httpx.AsyncClient(timeout=60.0) as client:
+ response = await client.post(
+ GEMINI_API_URL,
+ json=payload,
+ headers=headers,
+ params=params
+ )
+ response.raise_for_status()
+ result = response.json()
+
+ # Extract text from Gemini response
+ text_content = ""
+ if result.get("candidates") and len(result["candidates"]) > 0:
+ parts = result["candidates"][0].get("content", {}).get("parts", [])
+ if parts:
+ # Check if response is already JSON (when responseMimeType is set)
+ if "text" in parts[0]:
+ text_content = parts[0].get("text", "")
+ else:
+ # Fallback for other response formats
+ text_content = json.dumps(parts[0])
+
+ # Parse JSON from response
+ try:
+ # Remove markdown code blocks if present
+ if text_content.startswith("```"):
+ # Find the closing ```
+ parts = text_content.split("```")
+ if len(parts) >= 3:
+ text_content = parts[1]
+ if text_content.startswith("json"):
+ text_content = text_content[4:]
+ else:
+ text_content = text_content[3:]
+ text_content = text_content.strip()
+
+ # Try to find JSON object in the response
+ if not text_content.startswith("{"):
+ # Try to extract JSON from the text
+ start_idx = text_content.find("{")
+ end_idx = text_content.rfind("}")
+ if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
+ text_content = text_content[start_idx:end_idx+1]
+
+ extracted_data = json.loads(text_content)
+
+ # Merge with existing profile (don't overwrite existing non-null values)
+ update_data = {}
+ for key, value in extracted_data.items():
+ # Skip null, empty strings, and empty arrays/objects
+ if value is None:
+ continue
+ if isinstance(value, str) and value.strip() == "":
+ continue
+ if isinstance(value, list) and len(value) == 0:
+ continue
+ if isinstance(value, dict) and len(value) == 0:
+ continue
+
+ # Check if field exists and has a value in current profile
+ current_value = brand.get(key)
+ should_update = False
+
+ if current_value is None or current_value == "":
+ should_update = True
+ elif isinstance(current_value, list) and len(current_value) == 0:
+ should_update = True
+ elif isinstance(current_value, dict) and len(current_value) == 0:
+ should_update = True
+ elif isinstance(current_value, bool) and not current_value and isinstance(value, bool) and value:
+ # Allow updating false booleans to true
+ should_update = True
+
+ if should_update:
+ update_data[key] = value
+
+ if not update_data:
+ return {"message": "No new data could be extracted", "data": {}}
+
+ return {"message": f"Profile data extracted successfully. {len(update_data)} fields updated.", "data": update_data}
+ except json.JSONDecodeError as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to parse AI response as JSON: {str(e)}. Response: {text_content[:200]}"
+ )
+ except httpx.RequestError as e:
+ raise HTTPException(
+ status_code=502,
+ detail=f"Gemini API error: {str(e)}"
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error generating profile data: {str(e)}"
+ ) from e
+
+
+@router.post("/creator/profile/ai-fill")
+async def ai_fill_creator_profile(
+ request: AIFillRequest,
+ creator: dict = Depends(get_current_creator)
+):
+ """Use AI to fill creator profile based on user input"""
+ if not GEMINI_API_KEY:
+ raise HTTPException(
+ status_code=500,
+ detail="Gemini API is not configured"
+ )
+
+ try:
+ # Build prompt for Gemini
+ prompt = f"""You are an expert at extracting structured data from natural language. Your task is to analyze user input and extract ALL relevant creator profile information.
+
+Current creator profile (for context - only fill fields that are empty or null):
+{json.dumps(creator, indent=2, default=str)}
+
+User provided information:
+{request.user_input}
+
+Extract and return a JSON object with ALL fields that can be confidently determined from the user input. Use these exact field names and data types:
+
+STRING fields:
+- display_name, tagline, bio, profile_picture_url, cover_image_url, website_url
+- youtube_url, youtube_handle, instagram_url, instagram_handle, tiktok_url, tiktok_handle
+- twitter_url, twitter_handle, twitch_url, twitch_handle, linkedin_url, facebook_url
+- audience_age_primary, posting_frequency, best_performing_content_type, equipment_quality
+- preferred_payment_terms, media_kit_url
+
+NUMBER fields (use numbers, not strings):
+- youtube_subscribers (integer), instagram_followers (integer), tiktok_followers (integer)
+- twitter_followers (integer), twitch_followers (integer)
+- total_followers (integer), total_reach (integer), average_views (integer)
+- engagement_rate (float 0-100), average_engagement_per_post (integer)
+- years_of_experience (integer), team_size (integer)
+- rate_per_post (numeric), rate_per_video (numeric), rate_per_story (numeric), rate_per_reel (numeric)
+- minimum_deal_value (numeric), matching_score_base (float)
+
+BOOLEAN fields (use true/false):
+- content_creation_full_time, rate_negotiable, accepts_product_only_deals
+- email_verified, phone_verified, identity_verified
+- is_active, is_featured, is_verified_creator
+
+ARRAY fields (use JSON arrays of strings):
+- secondary_niches, content_types, content_language, audience_age_secondary
+- audience_interests, editing_software, collaboration_types
+- preferred_brands_style, not_interested_in, portfolio_links
+- past_brand_collaborations, case_study_links, search_keywords
+
+JSON OBJECT fields (use proper JSON objects):
+- audience_gender_split: {{"male": 45, "female": 50, "other": 5}} (percentages)
+- audience_locations: {{"country": "percentage", ...}} or {{"city": "percentage", ...}}
+- peak_posting_times: {{"monday": ["09:00", "18:00"], ...}} or {{"day": "time", ...}}
+- social_platforms: {{"platform": {{"handle": "...", "followers": 12345}}, ...}}
+
+IMPORTANT RULES:
+1. Extract ALL fields that can be inferred from the input, not just obvious ones
+2. For arrays, extract multiple values if mentioned (e.g., "lifestyle and tech" → ["lifestyle", "tech"])
+3. For numbers, extract numeric values (e.g., "$500 per post" → 500, "5 years" → 5)
+4. For booleans, infer from context (e.g., "I do this full-time" → true)
+5. For social media, extract handles, URLs, and follower counts if mentioned
+6. For audience data, structure as JSON objects with appropriate keys
+7. Only include fields that have clear values - omit uncertain fields
+8. Return ONLY valid JSON, no markdown, no explanations
+
+Return the JSON object now:"""
+
+ payload = {
+ "contents": [
+ {
+ "role": "user",
+ "parts": [{"text": prompt}]
+ }
+ ],
+ "generationConfig": {
+ "temperature": 0.1,
+ "topK": 40,
+ "topP": 0.95,
+ "maxOutputTokens": 8192,
+ "responseMimeType": "application/json"
+ }
+ }
+ headers = {"Content-Type": "application/json"}
+ params = {"key": GEMINI_API_KEY}
+
+ async with httpx.AsyncClient(timeout=60.0) as client:
+ response = await client.post(
+ GEMINI_API_URL,
+ json=payload,
+ headers=headers,
+ params=params
+ )
+ response.raise_for_status()
+ result = response.json()
+
+ # Extract text from Gemini response
+ text_content = ""
+ if result.get("candidates") and len(result["candidates"]) > 0:
+ parts = result["candidates"][0].get("content", {}).get("parts", [])
+ if parts:
+ # Check if response is already JSON (when responseMimeType is set)
+ if "text" in parts[0]:
+ text_content = parts[0].get("text", "")
+ else:
+ # Fallback for other response formats
+ text_content = json.dumps(parts[0])
+
+ # Parse JSON from response
+ try:
+ # Remove markdown code blocks if present
+ if text_content.startswith("```"):
+ # Find the closing ```
+ parts = text_content.split("```")
+ if len(parts) >= 3:
+ text_content = parts[1]
+ if text_content.startswith("json"):
+ text_content = text_content[4:]
+ else:
+ text_content = text_content[3:]
+ text_content = text_content.strip()
+
+ # Try to find JSON object in the response
+ if not text_content.startswith("{"):
+ # Try to extract JSON from the text
+ start_idx = text_content.find("{")
+ end_idx = text_content.rfind("}")
+ if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
+ text_content = text_content[start_idx:end_idx+1]
+
+ extracted_data = json.loads(text_content)
+
+ # Merge with existing profile (don't overwrite existing non-null values)
+ update_data = {}
+ for key, value in extracted_data.items():
+ # Skip null, empty strings, and empty arrays/objects
+ if value is None:
+ continue
+ if isinstance(value, str) and value.strip() == "":
+ continue
+ if isinstance(value, list) and len(value) == 0:
+ continue
+ if isinstance(value, dict) and len(value) == 0:
+ continue
+
+ # Check if field exists and has a value in current profile
+ current_value = creator.get(key)
+ should_update = False
+
+ if current_value is None or current_value == "":
+ should_update = True
+ elif isinstance(current_value, list) and len(current_value) == 0:
+ should_update = True
+ elif isinstance(current_value, dict) and len(current_value) == 0:
+ should_update = True
+ elif isinstance(current_value, bool) and not current_value and isinstance(value, bool) and value:
+ # Allow updating false booleans to true
+ should_update = True
+
+ if should_update:
+ update_data[key] = value
+
+ if not update_data:
+ return {"message": "No new data could be extracted", "data": {}}
+
+ return {"message": f"Profile data extracted successfully. {len(update_data)} fields updated.", "data": update_data}
+ except json.JSONDecodeError as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to parse AI response as JSON: {str(e)}. Response: {text_content[:200]}"
+ )
+ except httpx.RequestError as e:
+ raise HTTPException(
+ status_code=502,
+ detail=f"Gemini API error: {str(e)}"
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error generating profile data: {str(e)}"
+ ) from e
+
diff --git a/backend/app/main.py b/backend/app/main.py
index 5e2f73f..64dd92d 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -11,6 +11,7 @@
from app.api.routes import creators
from app.api.routes import proposals
from app.api.routes import analytics
+from app.api.routes import profiles
app = FastAPI(title="Inpact Backend", version="0.1.0")
# Verify Supabase client initialization on startup
@@ -42,6 +43,7 @@
app.include_router(creators.router)
app.include_router(proposals.router)
app.include_router(analytics.router)
+app.include_router(profiles.router)
@app.get("/")
def root():
diff --git a/frontend/app/brand/home/page.tsx b/frontend/app/brand/home/page.tsx
index a6d4440..9085543 100644
--- a/frontend/app/brand/home/page.tsx
+++ b/frontend/app/brand/home/page.tsx
@@ -3,6 +3,7 @@
import AuthGuard from "@/components/auth/AuthGuard";
import SlidingMenu from "@/components/SlidingMenu";
import BrandDashboard from "@/components/dashboard/BrandDashboard";
+import ProfileButton from "@/components/profile/ProfileButton";
import { getUserProfile, signOut } from "@/lib/auth-helpers";
import { Briefcase, Loader2, LogOut } from "lucide-react";
import { useRouter } from "next/navigation";
@@ -47,18 +48,21 @@ export default function BrandHomePage() {
InPactAI
-
+
+
+
+
diff --git a/frontend/app/brand/profile/page.tsx b/frontend/app/brand/profile/page.tsx
new file mode 100644
index 0000000..3e103e5
--- /dev/null
+++ b/frontend/app/brand/profile/page.tsx
@@ -0,0 +1,1314 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import ArrayInput from "@/components/profile/ArrayInput";
+import CollapsibleSection from "@/components/profile/CollapsibleSection";
+import JsonInput from "@/components/profile/JsonInput";
+import ProfileButton from "@/components/profile/ProfileButton";
+import SlidingMenu from "@/components/SlidingMenu";
+import {
+ aiFillBrandProfile,
+ BrandProfile,
+ getBrandProfile,
+ updateBrandProfile,
+} from "@/lib/api/profile";
+import { signOut } from "@/lib/auth-helpers";
+import {
+ ArrowLeft,
+ Briefcase,
+ Edit2,
+ Loader2,
+ LogOut,
+ Save,
+ Sparkles,
+ X,
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function BrandProfilePage() {
+ const router = useRouter();
+ const [profile, setProfile] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [formData, setFormData] = useState>({});
+ const [aiLoading, setAiLoading] = useState(false);
+ const [aiInput, setAiInput] = useState("");
+ const [showAiModal, setShowAiModal] = useState(false);
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+
+ useEffect(() => {
+ loadProfile();
+ }, []);
+
+ const loadProfile = async () => {
+ try {
+ setLoading(true);
+ const data = await getBrandProfile();
+ setProfile(data);
+ setFormData(data);
+ } catch (error) {
+ console.error("Error loading profile:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSave = async () => {
+ try {
+ setSaving(true);
+ const updated = await updateBrandProfile(formData);
+ setProfile(updated);
+ setFormData(updated);
+ setIsEditing(false);
+ } catch (error) {
+ console.error("Error saving profile:", error);
+ alert("Failed to save profile. Please try again.");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (profile) {
+ setFormData(profile);
+ setIsEditing(false);
+ }
+ };
+
+ const handleAiFill = async () => {
+ if (!aiInput.trim()) {
+ alert("Please provide some information about your brand");
+ return;
+ }
+
+ try {
+ setAiLoading(true);
+ const result = await aiFillBrandProfile(aiInput);
+
+ if (!result.data || Object.keys(result.data).length === 0) {
+ alert(
+ result.message ||
+ "No new data could be extracted from your input. Please provide more specific information."
+ );
+ return;
+ }
+
+ // Merge AI-generated data into form, handling all data types properly
+ setFormData((prev) => {
+ const updated = { ...prev };
+ for (const [key, value] of Object.entries(result.data)) {
+ // Properly handle arrays, objects, and primitives
+ if (Array.isArray(value)) {
+ updated[key] = value;
+ } else if (typeof value === "object" && value !== null) {
+ updated[key] = value;
+ } else {
+ updated[key] = value;
+ }
+ }
+ return updated;
+ });
+
+ setAiInput("");
+ setShowAiModal(false);
+
+ // Auto-enable edit mode if not already
+ if (!isEditing) {
+ setIsEditing(true);
+ }
+
+ // Show success message
+ const fieldCount = Object.keys(result.data).length;
+ alert(
+ `Success! ${fieldCount} field${fieldCount !== 1 ? "s" : ""} ${fieldCount !== 1 ? "were" : "was"} filled. Please review and save your changes.`
+ );
+ } catch (error: any) {
+ console.error("Error with AI fill:", error);
+ const errorMessage =
+ error?.message || "Failed to generate profile data. Please try again.";
+ alert(errorMessage);
+ } finally {
+ setAiLoading(false);
+ }
+ };
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+ await signOut();
+ router.push("/login");
+ } catch (error) {
+ console.error("Logout error:", error);
+ setIsLoggingOut(false);
+ }
+ };
+
+ const updateField = (field: string, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const updateArrayField = (field: string, values: string[]) => {
+ setFormData((prev) => ({ ...prev, [field]: values }));
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (!profile) {
+ return (
+
+
+
Failed to load profile
+
+
+ );
+ }
+
+ const completionPercentage = profile.profile_completion_percentage || 0;
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+ InPactAI
+
+
+
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Back Button */}
+
+
+ {/* Profile Header */}
+
+
+
+ Brand Profile
+
+
+ {!isEditing ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+ {/* Completion Bar */}
+
+
+
+ Profile Completion
+
+
+ {completionPercentage}%
+
+
+
+
+
+
+ {/* Profile Form */}
+
+ {/* Basic Information */}
+
+
+
+
+
+ updateField("company_name", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("industry", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateArrayField("sub_industry", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ updateField("company_tagline", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("company_type", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., B2B, B2C, SaaS"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("website_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("company_size", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., 1-10, 11-50, 51-200, 201-500, 500+"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "founded_year",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("headquarters_location", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("contact_email", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("contact_phone", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+
+
+
+ updateField("company_logo_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("company_cover_image_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("social_media_links", value)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Brand Identity */}
+
+
+
+
+ updateArrayField("brand_values", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("brand_personality", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+
+ updateField("brand_colors", value)}
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Target Audience */}
+
+
+
+
+
+
+
+ updateArrayField("target_audience_age_groups", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("target_audience_gender", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("target_audience_locations", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("target_audience_interests", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("target_audience_income_level", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Marketing & Campaigns */}
+
+
+
+
+ updateArrayField("marketing_goals", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("campaign_types_interested", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_content_types", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_platforms", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ updateField("campaign_frequency", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., Monthly, Quarterly"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "monthly_marketing_budget",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "influencer_budget_percentage",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "budget_per_campaign_min",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "budget_per_campaign_max",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "typical_deal_size",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("payment_terms", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ {/* Creator Preferences */}
+
+
+
+
+ updateArrayField("preferred_creator_niches", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_creator_size", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_creator_locations", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "minimum_followers_required",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "minimum_engagement_rate",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField(
+ "offers_product_only_deals",
+ e.target.checked
+ )
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+
+
+
+
+
+ updateField(
+ "offers_affiliate_programs",
+ e.target.checked
+ )
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "affiliate_commission_rate",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ {/* Content Guidelines */}
+
+
+
+
+ updateArrayField("content_dos", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("content_donts", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("brand_safety_requirements", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("competitor_brands", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+
+ {/* Products & Services */}
+
+
+
+
+ updateArrayField("products_services", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ updateField("product_price_range", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., $10-$50, $50-$100"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateArrayField("product_categories", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ updateField("seasonal_products", e.target.checked)
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+
+
+
+
+
+ updateField("product_catalog_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ {/* History & Performance */}
+
+
+
+
+ {/* Additional Settings */}
+
+
+
+
+ updateArrayField("search_keywords", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+
+
+
+
+ updateField(
+ "matching_score_base",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+
+
+
+
+
+ {/* AI Fill Modal */}
+ {showAiModal && (
+
+
+
+ AI Profile Filling
+
+
+ Provide information about your brand, and AI will help fill in
+ your profile fields automatically.
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/app/creator/home/page.tsx b/frontend/app/creator/home/page.tsx
index 4ffcdf5..4bb61ea 100644
--- a/frontend/app/creator/home/page.tsx
+++ b/frontend/app/creator/home/page.tsx
@@ -3,6 +3,7 @@
import AuthGuard from "@/components/auth/AuthGuard";
import SlidingMenu from "@/components/SlidingMenu";
import CreatorDashboard from "@/components/dashboard/CreatorDashboard";
+import ProfileButton from "@/components/profile/ProfileButton";
import { getUserProfile, signOut } from "@/lib/auth-helpers";
import { Loader2, LogOut, Sparkles } from "lucide-react";
import { useRouter } from "next/navigation";
@@ -47,18 +48,21 @@ export default function CreatorHomePage() {
InPactAI
-
+
+
+
+
diff --git a/frontend/app/creator/profile/page.tsx b/frontend/app/creator/profile/page.tsx
new file mode 100644
index 0000000..adab22f
--- /dev/null
+++ b/frontend/app/creator/profile/page.tsx
@@ -0,0 +1,1284 @@
+"use client";
+
+import AuthGuard from "@/components/auth/AuthGuard";
+import ArrayInput from "@/components/profile/ArrayInput";
+import CollapsibleSection from "@/components/profile/CollapsibleSection";
+import JsonInput from "@/components/profile/JsonInput";
+import ProfileButton from "@/components/profile/ProfileButton";
+import SlidingMenu from "@/components/SlidingMenu";
+import {
+ aiFillCreatorProfile,
+ CreatorProfile,
+ getCreatorProfile,
+ updateCreatorProfile,
+} from "@/lib/api/profile";
+import { signOut } from "@/lib/auth-helpers";
+import {
+ ArrowLeft,
+ Edit2,
+ Loader2,
+ LogOut,
+ Save,
+ Sparkles,
+ X,
+} from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+export default function CreatorProfilePage() {
+ const router = useRouter();
+ const [profile, setProfile] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [formData, setFormData] = useState>({});
+ const [aiLoading, setAiLoading] = useState(false);
+ const [aiInput, setAiInput] = useState("");
+ const [showAiModal, setShowAiModal] = useState(false);
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+
+ useEffect(() => {
+ loadProfile();
+ }, []);
+
+ const loadProfile = async () => {
+ try {
+ setLoading(true);
+ const data = await getCreatorProfile();
+ setProfile(data);
+ setFormData(data);
+ } catch (error) {
+ console.error("Error loading profile:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSave = async () => {
+ try {
+ setSaving(true);
+ const updated = await updateCreatorProfile(formData);
+ setProfile(updated);
+ setFormData(updated);
+ setIsEditing(false);
+ } catch (error) {
+ console.error("Error saving profile:", error);
+ alert("Failed to save profile. Please try again.");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (profile) {
+ setFormData(profile);
+ setIsEditing(false);
+ }
+ };
+
+ const handleAiFill = async () => {
+ if (!aiInput.trim()) {
+ alert("Please provide some information about yourself");
+ return;
+ }
+
+ try {
+ setAiLoading(true);
+ const result = await aiFillCreatorProfile(aiInput);
+
+ if (!result.data || Object.keys(result.data).length === 0) {
+ alert(
+ result.message ||
+ "No new data could be extracted from your input. Please provide more specific information."
+ );
+ return;
+ }
+
+ // Merge AI-generated data into form, handling all data types properly
+ setFormData((prev) => {
+ const updated = { ...prev };
+ for (const [key, value] of Object.entries(result.data)) {
+ // Properly handle arrays, objects, and primitives
+ if (Array.isArray(value)) {
+ updated[key] = value;
+ } else if (typeof value === "object" && value !== null) {
+ updated[key] = value;
+ } else {
+ updated[key] = value;
+ }
+ }
+ return updated;
+ });
+
+ setAiInput("");
+ setShowAiModal(false);
+
+ // Auto-enable edit mode if not already
+ if (!isEditing) {
+ setIsEditing(true);
+ }
+
+ // Show success message
+ const fieldCount = Object.keys(result.data).length;
+ alert(
+ `Success! ${fieldCount} field${fieldCount !== 1 ? "s" : ""} ${fieldCount !== 1 ? "were" : "was"} filled. Please review and save your changes.`
+ );
+ } catch (error: any) {
+ console.error("Error with AI fill:", error);
+ const errorMessage =
+ error?.message || "Failed to generate profile data. Please try again.";
+ alert(errorMessage);
+ } finally {
+ setAiLoading(false);
+ }
+ };
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+ await signOut();
+ router.push("/login");
+ } catch (error) {
+ console.error("Logout error:", error);
+ setIsLoggingOut(false);
+ }
+ };
+
+ const updateField = (field: string, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const updateArrayField = (field: string, values: string[]) => {
+ setFormData((prev) => ({ ...prev, [field]: values }));
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (!profile) {
+ return (
+
+
+
Failed to load profile
+
+
+ );
+ }
+
+ const completionPercentage = profile.profile_completion_percentage || 0;
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+ InPactAI
+
+
+
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Back Button */}
+
+
+ {/* Profile Header */}
+
+
+
+ Creator Profile
+
+
+ {!isEditing ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+ {/* Completion Bar */}
+
+
+
+ Profile Completion
+
+
+ {completionPercentage}%
+
+
+
+
+
+
+ {/* Profile Form */}
+
+ {/* Basic Information */}
+
+
+
+
+
+ updateField("display_name", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("primary_niche", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("tagline", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("website_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+
+
+
+ updateField("profile_picture_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("cover_image_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateArrayField("secondary_niches", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("content_types", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("content_language", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Social Media */}
+
+
+
+
+
+ updateField("instagram_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="@username"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("instagram_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "instagram_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("youtube_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("youtube_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "youtube_subscribers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("tiktok_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("tiktok_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "tiktok_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("twitter_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("twitter_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "twitter_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("twitch_handle", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("twitch_url", e.target.value)}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "twitch_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("linkedin_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("facebook_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "total_followers",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+ updateField("social_platforms", value)}
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Audience & Analytics */}
+
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "total_reach",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "average_views",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "engagement_rate",
+ raw === "" ? undefined : parseFloat(raw)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ {
+ const raw = e.target.value;
+ updateField(
+ "average_engagement_per_post",
+ raw === "" ? undefined : parseInt(raw, 10)
+ );
+ }}
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("audience_age_primary", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateArrayField("audience_age_secondary", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateField("audience_gender_split", value)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateField("audience_locations", value)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("audience_interests", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ updateField(
+ "best_performing_content_type",
+ e.target.value
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateField("peak_posting_times", value)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Content & Rates */}
+
+
+
+
+
+ updateField(
+ "years_of_experience",
+ parseInt(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("posting_frequency", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., Daily, 3x/week"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField(
+ "rate_per_post",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField(
+ "rate_per_video",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField(
+ "rate_per_story",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField(
+ "rate_per_reel",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("rate_negotiable", e.target.checked)
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+
+
+
+
+
+ updateField(
+ "minimum_deal_value",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("preferred_payment_terms", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField(
+ "accepts_product_only_deals",
+ e.target.checked
+ )
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+
+
+
+
+
+ {/* Professional Details */}
+
+
+
+
+
+ updateField(
+ "content_creation_full_time",
+ e.target.checked
+ )
+ }
+ disabled={!isEditing}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
+
+
+
+
+
+ updateField("team_size", parseInt(e.target.value) || 1)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ updateField("equipment_quality", e.target.value)
+ }
+ disabled={!isEditing}
+ placeholder="e.g., Professional, Semi-Professional"
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+ updateArrayField("editing_software", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("collaboration_types", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("preferred_brands_style", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("not_interested_in", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ {/* Portfolio & Links */}
+
+
+
+
+ updateArrayField("portfolio_links", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("past_brand_collaborations", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+ updateArrayField("case_study_links", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ updateField("media_kit_url", e.target.value)
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+ {/* Additional Settings */}
+
+
+
+
+ updateArrayField("search_keywords", values)
+ }
+ disabled={!isEditing}
+ />
+
+
+
+
+ updateField(
+ "matching_score_base",
+ parseFloat(e.target.value) || undefined
+ )
+ }
+ disabled={!isEditing}
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50"
+ />
+
+
+
+
+
+
+
+
+
+ {/* AI Fill Modal */}
+ {showAiModal && (
+
+
+
+ AI Profile Filling
+
+
+ Provide information about yourself, and AI will help fill in
+ your profile fields automatically.
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/components/profile/ArrayInput.tsx b/frontend/components/profile/ArrayInput.tsx
new file mode 100644
index 0000000..98cee2a
--- /dev/null
+++ b/frontend/components/profile/ArrayInput.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import { X, Plus } from "lucide-react";
+import { useState } from "react";
+
+interface ArrayInputProps {
+ label: string;
+ values: string[];
+ onChange: (values: string[]) => void;
+ disabled?: boolean;
+ placeholder?: string;
+}
+
+export default function ArrayInput({
+ label,
+ values,
+ onChange,
+ disabled = false,
+ placeholder = "Enter value and press Enter",
+}: ArrayInputProps) {
+ const [inputValue, setInputValue] = useState("");
+
+ const handleAdd = () => {
+ if (inputValue.trim() && !values.includes(inputValue.trim())) {
+ onChange([...values, inputValue.trim()]);
+ setInputValue("");
+ }
+ };
+
+ const handleRemove = (index: number) => {
+ onChange(values.filter((_, i) => i !== index));
+ };
+
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ handleAdd();
+ }
+ };
+
+ return (
+
+
+
+ {values.map((value, index) => (
+
+ {value}
+ {!disabled && (
+
+ )}
+
+ ))}
+
+ {!disabled && (
+
+
setInputValue(e.target.value)}
+ onKeyPress={handleKeyPress}
+ placeholder={placeholder}
+ className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm"
+ />
+
+
+ )}
+
+ );
+}
+
diff --git a/frontend/components/profile/CollapsibleSection.tsx b/frontend/components/profile/CollapsibleSection.tsx
new file mode 100644
index 0000000..554789f
--- /dev/null
+++ b/frontend/components/profile/CollapsibleSection.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { ChevronDown, ChevronUp } from "lucide-react";
+import { useState } from "react";
+
+interface CollapsibleSectionProps {
+ title: string;
+ children: React.ReactNode;
+ defaultOpen?: boolean;
+}
+
+export default function CollapsibleSection({
+ title,
+ children,
+ defaultOpen = false,
+}: CollapsibleSectionProps) {
+ const [isOpen, setIsOpen] = useState(defaultOpen);
+
+ return (
+
+
+ {isOpen && (
+
{children}
+ )}
+
+ );
+}
diff --git a/frontend/components/profile/JsonInput.tsx b/frontend/components/profile/JsonInput.tsx
new file mode 100644
index 0000000..c4111b0
--- /dev/null
+++ b/frontend/components/profile/JsonInput.tsx
@@ -0,0 +1,64 @@
+"use client";
+import { useEffect } from "react";
+
+import { useState } from "react";
+
+interface JsonInputProps {
+ label: string;
+ value: Record | null;
+ onChange: (value: Record | null) => void;
+ disabled?: boolean;
+}
+
+export default function JsonInput({
+ label,
+ value,
+ onChange,
+ disabled = false,
+}: JsonInputProps) {
+ const [textValue, setTextValue] = useState(
+ value ? JSON.stringify(value, null, 2) : ""
+ );
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ setTextValue(value ? JSON.stringify(value, null, 2) : "");
+ setError(null);
+ }, [value]);
+
+ const handleBlur = () => {
+ if (!textValue.trim()) {
+ onChange(null);
+ setError(null);
+ return;
+ }
+
+ try {
+ const parsed = JSON.parse(textValue);
+ onChange(parsed);
+ setError(null);
+ } catch (e) {
+ setError("Invalid JSON format");
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/components/profile/ProfileButton.tsx b/frontend/components/profile/ProfileButton.tsx
new file mode 100644
index 0000000..d312ec8
--- /dev/null
+++ b/frontend/components/profile/ProfileButton.tsx
@@ -0,0 +1,132 @@
+"use client";
+
+import { getUserProfile } from "@/lib/auth-helpers";
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+interface ProfileButtonProps {
+ role: "Brand" | "Creator";
+}
+
+export default function ProfileButton({ role }: ProfileButtonProps) {
+ const router = useRouter();
+ const [userName, setUserName] = useState("");
+ const [profileImageUrl, setProfileImageUrl] = useState(null);
+ const [imageError, setImageError] = useState(false);
+ const [imageLoading, setImageLoading] = useState(false);
+
+ useEffect(() => {
+ async function loadProfile() {
+ const profile = await getUserProfile();
+ if (profile) {
+ setUserName(profile.name);
+ }
+
+ // Fetch profile image based on role
+ try {
+ const apiUrl =
+ process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
+ const { supabase } = await import("@/lib/supabaseClient");
+ const {
+ data: { session },
+ } = await supabase.auth.getSession();
+ const accessToken = session?.access_token;
+
+ const endpoint =
+ role === "Brand" ? "/brand/profile" : "/creator/profile";
+ const response = await fetch(`${apiUrl}${endpoint}`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ const imageUrl =
+ role === "Brand" ? data.company_logo_url : data.profile_picture_url;
+ if (imageUrl) {
+ setProfileImageUrl(imageUrl);
+ setImageLoading(true);
+ setImageError(false);
+ } else {
+ setImageError(true);
+ setImageLoading(false);
+ }
+ } else {
+ setImageError(true);
+ setImageLoading(false);
+ }
+ } catch (error) {
+ console.error("Error loading profile image:", error);
+ setImageError(true);
+ setImageLoading(false);
+ }
+ }
+ loadProfile();
+ }, [role]);
+
+ // Set timeout for image loading (3 seconds)
+ useEffect(() => {
+ if (profileImageUrl && imageLoading) {
+ const timer = setTimeout(() => {
+ setImageError(true);
+ setImageLoading(false);
+ }, 3000);
+
+ return () => clearTimeout(timer);
+ } else if (!profileImageUrl) {
+ setImageError(true);
+ setImageLoading(false);
+ }
+ }, [profileImageUrl, imageLoading]);
+
+ const handleClick = () => {
+ const path = role === "Brand" ? "/brand/profile" : "/creator/profile";
+ router.push(path);
+ };
+
+ const getInitial = () => {
+ if (userName) {
+ return userName.charAt(0).toUpperCase();
+ }
+ return role === "Brand" ? "B" : "C";
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/lib/api/profile.ts b/frontend/lib/api/profile.ts
new file mode 100644
index 0000000..d08fb71
--- /dev/null
+++ b/frontend/lib/api/profile.ts
@@ -0,0 +1,283 @@
+type Nullable = T | null;
+import { authenticatedFetch } from "../auth-helpers";
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
+
+export interface BrandProfile {
+ id: string;
+ user_id: string;
+ company_name: string;
+ company_tagline?: Nullable;
+ company_description?: Nullable;
+ company_logo_url?: Nullable;
+ company_cover_image_url?: Nullable;
+ industry: string;
+ sub_industry?: Nullable;
+ company_size?: Nullable;
+ founded_year?: Nullable;
+ headquarters_location?: Nullable;
+ company_type?: Nullable;
+ website_url: string;
+ contact_email?: Nullable;
+ contact_phone?: Nullable;
+ social_media_links?: Nullable>;
+ target_audience_age_groups?: Nullable;
+ target_audience_gender?: Nullable;
+ target_audience_locations?: Nullable;
+ target_audience_interests?: Nullable;
+ target_audience_income_level?: Nullable;
+ target_audience_description?: Nullable;
+ brand_values?: Nullable;
+ brand_personality?: Nullable;
+ brand_voice?: Nullable;
+ brand_colors?: Nullable>;
+ marketing_goals?: Nullable;
+ campaign_types_interested?: Nullable;
+ preferred_content_types?: Nullable;
+ preferred_platforms?: Nullable;
+ campaign_frequency?: Nullable;
+ monthly_marketing_budget?: Nullable;
+ influencer_budget_percentage?: Nullable;
+ budget_per_campaign_min?: Nullable;
+ budget_per_campaign_max?: Nullable;
+ typical_deal_size?: Nullable;
+ payment_terms?: Nullable;
+ offers_product_only_deals?: Nullable;
+ offers_affiliate_programs?: Nullable;
+ affiliate_commission_rate?: Nullable;
+ preferred_creator_niches?: Nullable;
+ preferred_creator_size?: Nullable;
+ preferred_creator_locations?: Nullable;
+ minimum_followers_required?: Nullable;
+ minimum_engagement_rate?: Nullable;
+ content_dos?: Nullable;
+ content_donts?: Nullable;
+ brand_safety_requirements?: Nullable;
+ competitor_brands?: Nullable;
+ exclusivity_required?: Nullable;
+ exclusivity_duration_months?: Nullable;
+ past_campaigns_count?: Nullable;
+ successful_partnerships?: Nullable;
+ case_studies?: Nullable;
+ average_campaign_roi?: Nullable;
+ products_services?: Nullable;
+ product_price_range?: Nullable;
+ product_categories?: Nullable;
+ seasonal_products?: Nullable;
+ product_catalog_url?: Nullable;
+ business_verified?: Nullable;
+ payment_verified?: Nullable;
+ tax_id_verified?: Nullable;
+ profile_completion_percentage: number;
+ is_active?: Nullable;
+ is_featured?: Nullable;
+ is_verified_brand?: Nullable;
+ subscription_tier?: Nullable;
+ featured_until?: Nullable;
+ ai_profile_summary?: Nullable;
+ search_keywords?: Nullable;
+ matching_score_base?: Nullable;
+ total_deals_posted?: Nullable;
+ total_deals_completed?: Nullable;
+ total_spent?: Nullable;
+ average_deal_rating?: Nullable;
+ created_at?: Nullable;
+ updated_at?: Nullable;
+ last_active_at?: Nullable;
+ [key: string]: any;
+}
+
+export interface CreatorProfile {
+ id: string;
+ user_id: string;
+ display_name: string;
+ bio?: Nullable;
+ tagline?: Nullable;
+ profile_picture_url?: Nullable;
+ cover_image_url?: Nullable;
+ website_url?: Nullable;
+ youtube_url?: Nullable;
+ youtube_handle?: Nullable;
+ youtube_subscribers?: Nullable;
+ instagram_url?: Nullable;
+ instagram_handle?: Nullable;
+ instagram_followers?: Nullable;
+ tiktok_url?: Nullable;
+ tiktok_handle?: Nullable;
+ tiktok_followers?: Nullable;
+ twitter_url?: Nullable;
+ twitter_handle?: Nullable;
+ twitter_followers?: Nullable;
+ twitch_url?: Nullable;
+ twitch_handle?: Nullable;
+ twitch_followers?: Nullable;
+ linkedin_url?: Nullable;
+ facebook_url?: Nullable;
+ primary_niche: string;
+ secondary_niches?: Nullable;
+ content_types?: Nullable;
+ content_language?: Nullable;
+ total_followers: number;
+ total_reach?: Nullable;
+ average_views?: Nullable;
+ engagement_rate?: Nullable;
+ audience_age_primary?: Nullable;
+ audience_age_secondary?: Nullable;
+ audience_gender_split?: Nullable>;
+ audience_locations?: Nullable>;
+ audience_interests?: Nullable;
+ average_engagement_per_post?: Nullable;
+ posting_frequency?: Nullable;
+ best_performing_content_type?: Nullable;
+ peak_posting_times?: Nullable>;
+ years_of_experience?: Nullable;
+ content_creation_full_time: boolean;
+ team_size: number;
+ equipment_quality?: Nullable;
+ editing_software?: Nullable;
+ collaboration_types?: Nullable;
+ preferred_brands_style?: Nullable;
+ not_interested_in?: Nullable;
+ rate_per_post?: Nullable;
+ rate_per_video?: Nullable;
+ rate_per_story?: Nullable;
+ rate_per_reel?: Nullable;
+ rate_negotiable: boolean;
+ accepts_product_only_deals: boolean;
+ minimum_deal_value?: Nullable;
+ preferred_payment_terms?: Nullable;
+ portfolio_links?: Nullable;
+ past_brand_collaborations?: Nullable;
+ case_study_links?: Nullable;
+ media_kit_url?: Nullable;
+ email_verified?: Nullable;
+ phone_verified?: Nullable;
+ identity_verified?: Nullable;
+ profile_completion_percentage: number;
+ is_active?: Nullable;
+ is_featured?: Nullable;
+ is_verified_creator?: Nullable;
+ featured_until?: Nullable;
+ ai_profile_summary?: Nullable;
+ search_keywords?: Nullable;
+ matching_score_base?: Nullable;
+ social_platforms?: Nullable>;
+ created_at?: Nullable;
+ updated_at?: Nullable;
+ last_active_at?: Nullable;
+ [key: string]: any;
+}
+
+/**
+ * Get brand profile
+ */
+export async function getBrandProfile(): Promise {
+ const response = await authenticatedFetch(`${API_BASE_URL}/brand/profile`, {
+ method: "GET",
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || "Failed to fetch brand profile");
+ }
+
+ return response.json();
+}
+
+/**
+ * Update brand profile
+ */
+export async function updateBrandProfile(
+ data: Partial
+): Promise {
+ const response = await authenticatedFetch(`${API_BASE_URL}/brand/profile`, {
+ method: "PUT",
+ body: JSON.stringify({ data }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || "Failed to update brand profile");
+ }
+
+ return response.json();
+}
+
+/**
+ * Get creator profile
+ */
+export async function getCreatorProfile(): Promise {
+ const response = await authenticatedFetch(`${API_BASE_URL}/creator/profile`, {
+ method: "GET",
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || "Failed to fetch creator profile");
+ }
+
+ return response.json();
+}
+
+/**
+ * Update creator profile
+ */
+export async function updateCreatorProfile(
+ data: Partial
+): Promise {
+ const response = await authenticatedFetch(`${API_BASE_URL}/creator/profile`, {
+ method: "PUT",
+ body: JSON.stringify({ data }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || "Failed to update creator profile");
+ }
+
+ return response.json();
+}
+
+/**
+ * Use AI to fill brand profile
+ */
+export async function aiFillBrandProfile(
+ userInput: string
+): Promise<{ message: string; data: Partial }> {
+ const response = await authenticatedFetch(
+ `${API_BASE_URL}/brand/profile/ai-fill`,
+ {
+ method: "POST",
+ body: JSON.stringify({ user_input: userInput }),
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || "Failed to generate profile data");
+ }
+
+ return response.json();
+}
+
+/**
+ * Use AI to fill creator profile
+ */
+export async function aiFillCreatorProfile(
+ userInput: string
+): Promise<{ message: string; data: Partial }> {
+ const response = await authenticatedFetch(
+ `${API_BASE_URL}/creator/profile/ai-fill`,
+ {
+ method: "POST",
+ body: JSON.stringify({ user_input: userInput }),
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || "Failed to generate profile data");
+ }
+
+ return response.json();
+}