Skip to content

Conversation

@Saahi30
Copy link
Collaborator

@Saahi30 Saahi30 commented Jul 3, 2025

📝 Description

This PR introduces several major improvements and new features to the InPactAI platform:

  • Implements a fully functional signup and signin flow, including Google authentication.
  • Adds a basic but extensible matching mechanism for creators and brands using Logic & FastAPI.
  • Redesigns the homepage for a modern, user-friendly experience.
  • Introduces a dynamic “Trending Niches” section powered by Gemini and Supabase.

These changes enhance both the user experience and the backend logic, making the platform more robust, intelligent, and visually appealing.

🔧 Changes Made

  • Implemented Google authentication and completed the signup/signin flow.
  • Developed FastAPI endpoints and logic for creator-brand matching.
  • Redesigned the homepage with a modern layout and improved navigation.
  • Added a dynamic Trending Niches section with glassmorphism UI and live data.
  • Improved error handling and added clear comments throughout the codebase.
  • Refactored and updated several frontend and backend components for maintainability.

📷 Screenshots or Visual Changes (if applicable)

  • Redesigned homepage with new hero section, trending niches, and brand showcase.
Screenshot 2025-07-03 at 11 46 08 PM
  • Personalized welcome for logged-in users.
Screenshot 2025-07-03 at 11 44 16 PM -Creator to Brand matching in the overview tab Screenshot 2025-07-03 at 11 45 41 PM

Summary by CodeRabbit

  • New Features

    • Added multi-step onboarding flows for creators and brands, including profile picture/logo upload, platform details, pricing, and data validation.
    • Introduced matching endpoints and UI for brands to view matched creators and creators to view matched sponsorships, with dynamic match scoring and detailed cards.
    • Added trending niches API and homepage section, fetching and displaying top content niches.
    • Implemented public and protected route components for improved authentication and onboarding flow.
    • Added a role selection page for onboarding.
    • Enhanced authentication context with onboarding checks and improved user verification.
    • Added Vite API proxy for seamless local development.
    • Added YouTube channel info proxy endpoint and integration.
  • Improvements

    • Refined homepage with personalized content, glassmorphism styling, and conditional rendering based on authentication.
    • Updated navigation and user menu to reflect authentication state and user details.
    • Streamlined signup and login flows, delegating navigation and onboarding logic to the authentication context.
    • Improved dashboard pages to display dynamic matches based on user role and selections.
    • Enhanced onboarding persistence and error handling.
    • Removed local password storage and simplified timestamp handling in user data.
  • Bug Fixes

    • Removed password storage from user data and database scripts for improved security.
    • Improved error handling and fallback logic in trending niches and matching endpoints.
  • Chores & Documentation

    • Updated environment example files for new API keys.
    • Improved README setup instructions for clarity.
    • Added demo data insertion utility for testing.
  • Style

    • Introduced new CSS animations and 3D text effects for enhanced UI appeal.
    • Redesigned homepage and onboarding UIs with modern gradients and responsive layouts.
    • Updated main navigation to indicate menu under construction.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 3, 2025

Walkthrough

This update introduces a comprehensive multi-role onboarding flow for brands and creators, implements dynamic creator-brand matching via new backend endpoints and services, and enhances both frontend and backend authentication and routing logic. It also adds trending niche analytics via Gemini API, improves environment/configuration handling, and refines the homepage, navigation, and dashboard experiences.

Changes

Files / Grouped Paths Change Summary
Backend/.env-example, Frontend/env-example Added new API key placeholders (GEMINI_API_KEY, VITE_YOUTUBE_API_KEY).
Backend/app/routes/ai.py New router for /api/trending-niches, integrating Gemini API and Supabase for trending content niches.
Backend/app/routes/match.py, Backend/app/services/db_service.py New matching endpoints and Supabase service for creator-brand matching logic.
Backend/app/routes/auth.py Added /auth/ping health check endpoint.
Backend/app/main.py Included new match and ai routers; switched to relative imports.
Backend/app/db/seed.py, Backend/sql.txt, Backend/app/schemas/schema.py Removed password_hash from user creation and schema; added profile_image and created_at fields.
Backend/app/models/models.py, Backend/app/models/chat.py Simplified timestamp handling; updated import paths.
Backend/app/routes/chat.py, Backend/app/routes/post.py, Backend/app/services/chat_services.py Standardized import paths to be relative or fully qualified.
Frontend/src/components/Onboarding.tsx Complete rewrite: now a multi-step, role-based onboarding flow for brands and creators with validation, file uploads, and Supabase integration.
Frontend/src/pages/RoleSelection.tsx New role selection page for onboarding.
Frontend/src/components/PublicRoute.tsx New component to manage access to public routes based on authentication and onboarding status.
Frontend/src/components/ProtectedRoute.tsx Now checks isAuthenticated instead of user for route protection.
Frontend/src/context/AuthContext.tsx Added onboarding status checks, improved user presence verification, and enhanced loading state management.
Frontend/src/pages/Signup.tsx, Frontend/src/pages/Login.tsx Simplified signup/login flows; navigation and onboarding logic now handled by AuthContext.
Frontend/src/pages/HomePage.tsx Major homepage redesign with trending niches, brand showcase, and conditional rendering for authenticated users.
Frontend/src/pages/Brand/Dashboard.tsx, Frontend/src/pages/DashboardPage.tsx Dashboard now shows dynamic creator-brand matches using new components and API endpoints.
Frontend/src/components/dashboard/creator-matches.tsx, Frontend/src/components/dashboard/sponsorship-matches.tsx New/updated components for displaying dynamic match results.
Frontend/src/components/user-nav.tsx, Frontend/src/components/main-nav.tsx UserNav now uses auth context; MainNav navigation links removed.
Frontend/src/App.tsx Added loading screen, new onboarding and role selection routes, and route protection logic.
Frontend/src/utils/demoInsert.ts New utility for demo data insertion into Supabase.
Frontend/src/utils/supabase.tsx Added runtime environment variable checks for Supabase config.
Frontend/src/index.css Added new animation and text effect utility classes.
Frontend/vite.config.ts Added proxy for /api requests to backend during development.
README.md Improved setup instructions, formatting, and added Groq API key info.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant Supabase
    participant GeminiAPI

    User->>Frontend: Access onboarding or dashboard
    Frontend->>Backend: /api/match/creators-for-brand or /api/match/brands-for-creator
    Backend->>Supabase: Fetch sponsorships/creators and insights
    Backend->>Backend: Compute match scores
    Backend->>Frontend: Return matches
    Frontend->>User: Render matched creators/brands

    User->>Frontend: Visit trending niches section
    Frontend->>Backend: /api/trending-niches
    Backend->>Supabase: Check for today's trending niches
    alt Not found
        Backend->>GeminiAPI: Request trending niches
        GeminiAPI-->>Backend: Return JSON data
        Backend->>Supabase: Store new trending niches
        Backend->>Frontend: Return trending niches
    else Found
        Backend->>Frontend: Return cached trending niches
    end
    Frontend->>User: Display trending niches
Loading

Possibly related PRs

  • AOSSIE-Org/InPactAI#81: Updates the README.md with improved setup instructions and formatting; related as both PRs modify the README for setup clarity.

Suggested labels

enhancement, frontend, backend, documentation

Poem

Hopping through code, I see new light,
Onboarding flows that feel just right.
Brands and creators now can meet,
With matches made—oh, what a treat!
Trending niches, dashboards, too,
This bunny’s proud of all you do.
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 22

🔭 Outside diff range comments (1)
Backend/app/db/seed.py (1)

43-51: Critical: Missing required password_hash field.

The User model in Backend/app/models/models.py lines 25-53 shows password_hash as a required field (nullable=False), but it's not being provided when creating users. This will cause database constraint violations.

 user = User(
     id=user_data["id"],
     username=user_data["username"],
     email=user_data["email"],
+    password_hash=user_data["password"],  # Add this line
     role=user_data["role"],
     profile_image=user_data["profile_image"],
     bio=user_data["bio"],
     created_at=user_data["created_at"]
 )

Alternatively, if password handling has changed, ensure the User model is updated to make password_hash nullable or has a default value.

♻️ Duplicate comments (1)
Frontend/src/components/dashboard/sponsorship-matches.tsx (1)

71-74: Add functionality to buttons or indicate they're placeholders

Similar to CreatorMatches, these buttons need onClick handlers or should be disabled.

🧹 Nitpick comments (18)
Backend/app/routes/auth.py (1)

1-7: Fix formatting issue and consider expanding auth functionality.

The basic auth router implementation is functional, but there's a formatting issue that should be addressed.

Apply this diff to fix the formatting issue:

 from fastapi import APIRouter

 router = APIRouter()

+
 @router.get("/auth/ping")
 def ping():
     return {"message": "Auth route is working!"}

Consider expanding this module to include actual authentication endpoints like login, signup, and token validation as the application grows.

Backend/app/main.py (1)

10-11: Remove unused imports

Static analysis indicates that logging and os are imported but never used in this file.

-import logging
-import os
Frontend/src/components/main-nav.tsx (1)

1-1: Remove unused import

The Link import from react-router-dom is no longer used after removing all navigation links.

-import { Link } from "react-router-dom"
Frontend/src/utils/demoInsert.ts (1)

3-40: Add error handling and duplicate prevention for demo data insertion.

The function works but could be more robust for production use.

Consider these improvements:

 export async function demoInsert() {
+  try {
+    // Check if demo user already exists
+    const { data: existingUser } = await supabase
+      .from('users')
+      .select('id')
+      .eq('id', 'demo-user-123')
+      .single();
+    
+    if (existingUser) {
+      console.log('Demo user already exists, skipping insertion');
+      return;
+    }
+
     // Insert user
     const { data: user, error: userError } = await supabase
       .from('users')
       .insert({
         // ... existing fields
       });
-    console.log('User:', user, userError);
+    
+    if (userError) {
+      console.error('Failed to insert demo user:', userError);
+      return;
+    }
+    console.log('Demo user inserted successfully');

     // Insert social profile
     const { data: profile, error: profileError } = await supabase
       .from('social_profiles')
       .insert({
         // ... existing fields
       });
-    console.log('Profile:', profile, profileError);
+    
+    if (profileError) {
+      console.error('Failed to insert demo profile:', profileError);
+      return;
+    }
+    console.log('Demo profile inserted successfully');
+  } catch (error) {
+    console.error('Demo insertion failed:', error);
+  }
 }
README.md (1)

164-166: Minor grammar improvement needed.

Consider adding "to" for better readability:

-6. Get the Groq API key:
+6. Get the Groq API key:
    - Visit [Groq Console](https://console.groq.com/)
-   - Create an API key and paste it into the `.env` file
+   - Create an API key and paste it into the `.env` file
Frontend/src/pages/Brand/Dashboard.tsx (1)

40-46: Mock data approach is appropriate for current development stage.

The hardcoded sponsorships provide a good foundation for testing the dynamic matching functionality.

Consider replacing with an API call when backend sponsorship endpoints are ready:

const [sponsorships, setSponsorships] = useState([]);

useEffect(() => {
  // Fetch real sponsorships from API
  fetch('/api/sponsorships')
    .then(res => res.json())
    .then(data => setSponsorships(data))
    .catch(err => console.error('Failed to fetch sponsorships:', err));
}, []);
Frontend/src/App.tsx (1)

24-42: Reconsider the arbitrary loading delay.

The 2-second loading delay may not provide the best user experience, especially for users with fast connections. Consider implementing a more dynamic loading approach that actually waits for necessary resources or user authentication status.

-  useEffect(() => {
-    // Set a timeout to ensure the app loads
-    const timer = setTimeout(() => {
-      setIsLoading(false);
-    }, 2000);
-
-    return () => clearTimeout(timer);
-  }, []);
+  useEffect(() => {
+    // Check if essential resources are loaded or perform actual initialization
+    const initializeApp = async () => {
+      // Add any actual initialization logic here
+      // e.g., checking auth state, loading user preferences, etc.
+      await new Promise(resolve => setTimeout(resolve, 300)); // Minimal delay for smooth UX
+      setIsLoading(false);
+    };
+
+    initializeApp();
+  }, []);
Backend/app/routes/match.py (1)

13-28: Fix formatting and import issues.

The router logic is sound, but there are formatting issues flagged by static analysis tools.

+

 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}

Also, consider using absolute imports instead of relative imports for better maintainability:

-from ..services.db_service import match_creators_for_brand, match_brands_for_creator
+from app.services.db_service import match_creators_for_brand, match_brands_for_creator
Backend/app/routes/ai.py (1)

39-66: Add input validation and improve error handling.

The endpoint needs better validation of API responses and more specific error handling.

+
 @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()
+            # Validate response structure
+            if not isinstance(niches, list) or len(niches) != 6:
+                raise ValueError("Invalid response format from Gemini API")
+            
             for niche in niches:
+                # Validate each niche has required fields
+                if not all(key in niche for key in ["name", "insight", "global_activity"]):
+                    raise ValueError(f"Missing required fields in niche: {niche}")
+                
                 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)
+            print(f"Gemini fetch failed: {type(e).__name__}: {str(e)}")
             # fallback: serve most recent data
             result = supabase.table("trending_niches").select("*").order("fetched_at", desc=True).limit(6).execute()
     return result.data
Frontend/src/components/dashboard/creator-matches.tsx (1)

58-58: Consider making the placeholder image URL configurable

The hardcoded placeholder URL should be moved to a configuration file or environment variable for easier maintenance.

Frontend/src/pages/Signup.tsx (2)

7-7: Remove unused import

The demoInsert import is not used in this component.

-import { demoInsert } from '../utils/demoInsert';

118-118: Display password strength indicator to users

The password strength is calculated but never shown to users. This is a missed opportunity to encourage stronger passwords.

Add the password strength indicator after the password input:

 <input id="password" name="password" type={showPassword ? "text" : "password"} value={formData.password} onChange={handleChange} required className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200" placeholder="Password" />
+{formData.password && (
+  <div className="mt-2">
+    <div className="flex items-center gap-2">
+      <div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
+        <div
+          className={`h-2 rounded-full transition-all duration-300 ${color}`}
+          style={{ width: `${(strength / 4) * 100}%` }}
+        />
+      </div>
+      <span className="text-xs text-gray-600 dark:text-gray-400">{text}</span>
+    </div>
+  </div>
+)}

Also applies to: 171-172

Frontend/src/components/PublicRoute.tsx (1)

40-42: Consider improving cache expiration mechanism

The current setTimeout approach for cache expiration might not be cleaned up if the component unmounts, potentially causing memory leaks.

Consider storing the expiration timestamp with the cached data instead:

-sessionStorage.setItem(cacheKey, JSON.stringify({ hasOnboarding, role }));
-setTimeout(() => sessionStorage.removeItem(cacheKey), 2 * 60 * 1000);
+sessionStorage.setItem(cacheKey, JSON.stringify({ 
+  hasOnboarding, 
+  role,
+  expiry: Date.now() + 2 * 60 * 1000 
+}));

Then check expiration when reading from cache:

 if (cachedResult) {
-  const { hasOnboarding, role } = JSON.parse(cachedResult);
+  const cached = JSON.parse(cachedResult);
+  if (cached.expiry < Date.now()) {
+    sessionStorage.removeItem(cacheKey);
+  } else {
+    const { hasOnboarding, role } = cached;
Frontend/src/context/AuthContext.tsx (2)

25-62: Consider using a more robust caching mechanism.

While sessionStorage works, it's cleared when the tab is closed. Consider using localStorage with expiration for better persistence across sessions.

-  // Add a simple cache to prevent repeated requests for the same user
-  const cacheKey = `user_${user.id}`;
-  if (sessionStorage.getItem(cacheKey)) {
-    console.log("User already checked, skipping...");
-    return;
-  }
+  // Add a more robust cache with expiration
+  const cacheKey = `user_check_${user.id}`;
+  const cachedData = localStorage.getItem(cacheKey);
+  if (cachedData) {
+    const { timestamp } = JSON.parse(cachedData);
+    // Check if cache is still valid (5 minutes)
+    if (Date.now() - timestamp < 5 * 60 * 1000) {
+      console.log("User already checked (cached), skipping...");
+      return;
+    }
+  }

   // ... existing code ...

-    // Cache the result for 5 minutes to prevent repeated requests
-    sessionStorage.setItem(cacheKey, "true");
-    setTimeout(() => sessionStorage.removeItem(cacheKey), 5 * 60 * 1000);
+    // Cache the result with timestamp
+    localStorage.setItem(cacheKey, JSON.stringify({
+      checked: true,
+      timestamp: Date.now()
+    }));

71-107: Optimize database queries and improve rate limiting.

The function makes three separate database queries which could be optimized. Also, the rate limiting mechanism could be more sophisticated.

Consider these improvements:

  1. Combine the queries to reduce round trips
  2. Use a more robust rate limiting mechanism
 const checkUserOnboarding = async (userToCheck?: User | null) => {
   const userToUse = userToCheck || user;
   if (!userToUse) return { hasOnboarding: false, role: null };
   
-  // Add rate limiting - only allow one request per 2 seconds
-  const now = Date.now();
-  if (now - lastRequest < 2000) {
-    console.log("Rate limiting: skipping request");
-    return { hasOnboarding: false, role: null };
-  }
-  setLastRequest(now);
+  // Improved rate limiting with request queue
+  const cacheKey = `onboarding_check_${userToUse.id}`;
+  const cached = sessionStorage.getItem(cacheKey);
+  if (cached) {
+    const data = JSON.parse(cached);
+    if (Date.now() - data.timestamp < 2000) {
+      return data.result;
+    }
+  }
   
-  // Check if user has completed onboarding by looking for social profiles or brand data
-  const { data: socialProfiles } = await supabase
-    .from("social_profiles")
-    .select("id")
-    .eq("user_id", userToUse.id)
-    .limit(1);
-  
-  const { data: brandData } = await supabase
-    .from("brands")
-    .select("id")
-    .eq("user_id", userToUse.id)
-    .limit(1);
-  
-  const hasOnboarding = (socialProfiles && socialProfiles.length > 0) || (brandData && brandData.length > 0);
-  
-  // Get user role
-  const { data: userData } = await supabase
-    .from("users")
-    .select("role")
-    .eq("id", userToUse.id)
-    .single();
+  try {
+    // Combine queries using Promise.all for better performance
+    const [socialProfilesResp, brandDataResp, userDataResp] = await Promise.all([
+      supabase.from("social_profiles").select("id").eq("user_id", userToUse.id).limit(1),
+      supabase.from("brands").select("id").eq("user_id", userToUse.id).limit(1),
+      supabase.from("users").select("role").eq("id", userToUse.id).single()
+    ]);
+    
+    const hasOnboarding = 
+      (socialProfilesResp.data && socialProfilesResp.data.length > 0) || 
+      (brandDataResp.data && brandDataResp.data.length > 0);
+    
+    const result = { hasOnboarding, role: userDataResp.data?.role || null };
+    
+    // Cache the result
+    sessionStorage.setItem(cacheKey, JSON.stringify({
+      result,
+      timestamp: Date.now()
+    }));
+    
+    return result;
+  } catch (error) {
+    console.error("Error checking user onboarding:", error);
+    return { hasOnboarding: false, role: null };
+  }
-  
-  return { hasOnboarding, role: userData?.role || null };
 };
Frontend/src/components/Onboarding.tsx (2)

901-901: Improve email validation regex.

The current email regex is basic and might not catch all invalid formats.

Consider using a more comprehensive email validation:

-if (!brandData.contact_email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(brandData.contact_email)) return "Valid contact email is required.";
+// More comprehensive email validation
+const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
+if (!brandData.contact_email || !emailRegex.test(brandData.contact_email)) return "Valid contact email is required.";

Or better yet, use a library like validator:

+import validator from 'validator';
+
-if (!brandData.contact_email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(brandData.contact_email)) return "Valid contact email is required.";
+if (!brandData.contact_email || !validator.isEmail(brandData.contact_email)) return "Valid contact email is required.";

1177-1180: Improve YouTube channel ID extraction.

The current regex might not handle all YouTube URL formats correctly.

Consider a more comprehensive URL parsing approach:

-    // Extract channel ID from URL if needed
-    if (input.includes("youtube.com")) {
-      const match = input.match(/(?:channel\/|user\/|c\/)?([\w-]{21,})/);
-      if (match) channelId = match[1];
-    }
+    // Extract channel ID from URL if needed
+    if (input.includes("youtube.com") || input.includes("youtu.be")) {
+      // Handle various YouTube URL formats
+      const patterns = [
+        /youtube\.com\/channel\/(UC[\w-]{21})/,
+        /youtube\.com\/c\/([\w-]+)/,
+        /youtube\.com\/user\/([\w-]+)/,
+        /youtube\.com\/@([\w-]+)/,
+      ];
+      
+      for (const pattern of patterns) {
+        const match = input.match(pattern);
+        if (match) {
+          channelId = match[1];
+          break;
+        }
+      }
+      
+      // If it's a custom URL (c/ or @), you might need to resolve it
+      // This would require an additional API call
+    }
Frontend/src/pages/HomePage.tsx (1)

73-262: Move static data outside component to prevent recreation on each render.

These large data arrays are recreated on every render, which is inefficient.

Move all static data arrays outside the component:

+// Move these outside the component
+const DASHBOARD_FEATURES = [
+  {
+    icon: TrendingUp,
+    title: "Analytics Dashboard",
+    desc: "Track your performance metrics, engagement rates, and growth trends.",
+  },
+  // ... rest of the features
+];
+
+const SUCCESS_STORIES = [
+  // ... success stories data
+];
+
+const TRENDING_NICHES = [
+  // ... trending niches data
+];
+
+const CREATOR_RESOURCES = [
+  // ... creator resources data
+];
+
+const BRAND_SHOWCASE = [
+  // ... brand showcase data
+];

 export default function HomePage() {
-  const dashboardFeatures = [
-    // ... remove from here
-  ];
-  
-  const successStories = [
-    // ... remove from here
-  ];
   
   // Use the constants instead
+  // dashboardFeatures -> DASHBOARD_FEATURES
+  // successStories -> SUCCESS_STORIES
+  // etc.

This prevents unnecessary memory allocation and improves performance.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af8eaf8 and 0eb0b1d.

⛔ Files ignored due to path filters (6)
  • Frontend/public/brand.png is excluded by !**/*.png
  • Frontend/public/contnetcreator.png is excluded by !**/*.png
  • Frontend/public/facebook.png is excluded by !**/*.png
  • Frontend/public/instagram.png is excluded by !**/*.png
  • Frontend/public/tiktok.png is excluded by !**/*.png
  • Frontend/public/youtube.png is excluded by !**/*.png
📒 Files selected for processing (35)
  • Backend/.env-example (1 hunks)
  • Backend/app/db/seed.py (4 hunks)
  • Backend/app/main.py (2 hunks)
  • Backend/app/models/chat.py (1 hunks)
  • Backend/app/models/models.py (2 hunks)
  • Backend/app/routes/ai.py (1 hunks)
  • Backend/app/routes/auth.py (1 hunks)
  • Backend/app/routes/chat.py (1 hunks)
  • Backend/app/routes/match.py (1 hunks)
  • Backend/app/routes/post.py (1 hunks)
  • Backend/app/schemas/schema.py (0 hunks)
  • Backend/app/services/chat_services.py (1 hunks)
  • Backend/app/services/db_service.py (1 hunks)
  • Backend/sql.txt (1 hunks)
  • Frontend/env-example (1 hunks)
  • Frontend/src/App.tsx (2 hunks)
  • Frontend/src/components/Onboarding.tsx (1 hunks)
  • Frontend/src/components/ProtectedRoute.tsx (1 hunks)
  • Frontend/src/components/PublicRoute.tsx (1 hunks)
  • Frontend/src/components/dashboard/creator-matches.tsx (1 hunks)
  • Frontend/src/components/dashboard/sponsorship-matches.tsx (1 hunks)
  • Frontend/src/components/main-nav.tsx (1 hunks)
  • Frontend/src/components/user-nav.tsx (1 hunks)
  • Frontend/src/context/AuthContext.tsx (4 hunks)
  • Frontend/src/index.css (1 hunks)
  • Frontend/src/pages/Brand/Dashboard.tsx (2 hunks)
  • Frontend/src/pages/DashboardPage.tsx (2 hunks)
  • Frontend/src/pages/HomePage.tsx (2 hunks)
  • Frontend/src/pages/Login.tsx (2 hunks)
  • Frontend/src/pages/RoleSelection.tsx (1 hunks)
  • Frontend/src/pages/Signup.tsx (3 hunks)
  • Frontend/src/utils/demoInsert.ts (1 hunks)
  • Frontend/src/utils/supabase.tsx (1 hunks)
  • Frontend/vite.config.ts (1 hunks)
  • README.md (5 hunks)
💤 Files with no reviewable changes (1)
  • Backend/app/schemas/schema.py
🧰 Additional context used
🧬 Code Graph Analysis (13)
Backend/app/routes/chat.py (3)
Backend/app/db/db.py (1)
  • get_db (38-40)
Backend/app/services/redis_client.py (1)
  • get_redis (6-7)
Backend/app/services/chat_pubsub.py (1)
  • listen_to_channel (6-16)
Frontend/src/components/ProtectedRoute.tsx (1)
Frontend/src/context/AuthContext.tsx (1)
  • useAuth (216-222)
Backend/app/routes/post.py (1)
Backend/app/models/models.py (7)
  • User (25-53)
  • AudienceInsights (57-72)
  • Sponsorship (76-92)
  • UserPost (96-110)
  • SponsorshipApplication (114-128)
  • SponsorshipPayment (146-162)
  • Collaboration (132-142)
Frontend/src/pages/DashboardPage.tsx (2)
Frontend/src/context/AuthContext.tsx (1)
  • useAuth (216-222)
Frontend/src/components/dashboard/sponsorship-matches.tsx (1)
  • SponsorshipMatches (20-83)
Backend/app/db/seed.py (1)
Backend/app/models/models.py (1)
  • User (25-53)
Backend/app/routes/match.py (1)
Backend/app/services/db_service.py (2)
  • match_creators_for_brand (13-48)
  • match_brands_for_creator (51-85)
Frontend/src/utils/demoInsert.ts (1)
Frontend/src/utils/supabase.tsx (1)
  • supabase (11-11)
Frontend/src/pages/Brand/Dashboard.tsx (1)
Frontend/src/components/dashboard/creator-matches.tsx (1)
  • CreatorMatches (22-85)
Frontend/src/pages/Signup.tsx (2)
Frontend/src/context/AuthContext.tsx (1)
  • useAuth (216-222)
Frontend/src/utils/supabase.tsx (1)
  • supabase (11-11)
Frontend/src/components/dashboard/sponsorship-matches.tsx (4)
Frontend/src/components/ui/card.tsx (2)
  • Card (80-80)
  • CardContent (85-85)
Frontend/src/components/ui/avatar.tsx (3)
  • Avatar (50-50)
  • AvatarImage (50-50)
  • AvatarFallback (50-50)
Frontend/src/components/ui/badge.tsx (1)
  • Badge (36-36)
Frontend/src/components/ui/button.tsx (1)
  • Button (54-54)
Frontend/src/components/dashboard/creator-matches.tsx (4)
Frontend/src/components/ui/card.tsx (2)
  • Card (80-80)
  • CardContent (85-85)
Frontend/src/components/ui/avatar.tsx (3)
  • Avatar (50-50)
  • AvatarImage (50-50)
  • AvatarFallback (50-50)
Frontend/src/components/ui/badge.tsx (1)
  • Badge (36-36)
Frontend/src/components/ui/button.tsx (1)
  • Button (54-54)
Frontend/src/components/PublicRoute.tsx (1)
Frontend/src/context/AuthContext.tsx (1)
  • useAuth (216-222)
Frontend/src/context/AuthContext.tsx (2)
Backend/app/models/models.py (1)
  • User (25-53)
Frontend/src/utils/supabase.tsx (1)
  • supabase (11-11)
🪛 Pylint (3.3.7)
Backend/app/routes/chat.py

[error] 10-10: Attempted relative import beyond top-level package

(E0402)


[error] 11-11: Attempted relative import beyond top-level package

(E0402)


[error] 13-13: Attempted relative import beyond top-level package

(E0402)


[error] 15-15: Attempted relative import beyond top-level package

(E0402)

Backend/app/routes/post.py

[error] 4-4: Attempted relative import beyond top-level package

(E0402)


[error] 5-8: Attempted relative import beyond top-level package

(E0402)

Backend/app/routes/match.py

[error] 5-5: Attempted relative import beyond top-level package

(E0402)

Backend/app/services/db_service.py

[refactor] 30-30: Consider using a generator instead 'sum(creator_ages.get(age, 0) for age in required_ages)'

(R1728)


[refactor] 37-37: Consider using a generator instead 'sum(creator_locs.get(loc, 0) for loc in required_locs)'

(R1728)


[refactor] 67-67: Consider using a generator instead 'sum(creator_ages.get(age, 0) for age in required_ages)'

(R1728)


[refactor] 74-74: Consider using a generator instead 'sum(creator_locs.get(loc, 0) for loc in required_locs)'

(R1728)

🪛 Flake8 (7.2.0)
Backend/app/routes/auth.py

[error] 5-5: expected 2 blank lines, found 1

(E302)

Backend/app/routes/post.py

[error] 4-4: '..db.db.AsyncSessionLocal' imported but unused

(F401)


[error] 5-5: '..models.models.User' imported but unused

(F401)


[error] 5-5: '..models.models.AudienceInsights' imported but unused

(F401)


[error] 5-5: '..models.models.Sponsorship' imported but unused

(F401)


[error] 5-5: '..models.models.UserPost' imported but unused

(F401)


[error] 5-5: '..models.models.SponsorshipApplication' imported but unused

(F401)


[error] 5-5: '..models.models.SponsorshipPayment' imported but unused

(F401)


[error] 5-5: '..models.models.Collaboration' imported but unused

(F401)

Backend/app/main.py

[error] 10-10: 'logging' imported but unused

(F401)


[error] 11-11: 'os' imported but unused

(F401)

Backend/app/routes/match.py

[error] 15-15: expected 2 blank lines, found 1

(E302)


[error] 22-22: expected 2 blank lines, found 1

(E302)

Backend/app/routes/ai.py

[error] 18-18: expected 2 blank lines, found 1

(E302)


[error] 21-21: too many blank lines (2)

(E303)


[error] 39-39: expected 2 blank lines, found 1

(E302)

🪛 Ruff (0.11.9)
Backend/app/routes/post.py

4-4: ..db.db.AsyncSessionLocal imported but unused

Remove unused import: ..db.db.AsyncSessionLocal

(F401)


6-6: ..models.models.User imported but unused

Remove unused import

(F401)


6-6: ..models.models.AudienceInsights imported but unused

Remove unused import

(F401)


6-6: ..models.models.Sponsorship imported but unused

Remove unused import

(F401)


6-6: ..models.models.UserPost imported but unused

Remove unused import

(F401)


7-7: ..models.models.SponsorshipApplication imported but unused

Remove unused import

(F401)


7-7: ..models.models.SponsorshipPayment imported but unused

Remove unused import

(F401)


7-7: ..models.models.Collaboration imported but unused

Remove unused import

(F401)

Backend/app/main.py

10-10: logging imported but unused

Remove unused import: logging

(F401)


11-11: os imported but unused

Remove unused import: os

(F401)

🪛 LanguageTool
README.md

[uncategorized] ~166-~166: Possible missing preposition found.
Context: ...onsole](https://console.groq.com/) - Create an API key and paste it into the .env...

(AI_HYDRA_LEO_MISSING_TO)

🪛 Biome (1.9.4)
Frontend/src/components/Onboarding.tsx

[error] 392-392: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 645-645: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (30)
Backend/.env-example (1)

9-9: LGTM! Environment variable addition is properly formatted.

The addition of GEMINI_API_KEY follows the consistent pattern of other API keys in the file and supports the new AI-powered trending niches functionality.

Frontend/env-example (1)

3-3: LGTM! Frontend environment variable follows proper conventions.

The addition of VITE_YOUTUBE_API_KEY correctly uses the VITE_ prefix and provides a clear placeholder value for the YouTube API integration in the onboarding flow.

Backend/app/models/chat.py (1)

4-4: LGTM! Import standardization improves maintainability.

The change from relative to absolute import path enhances code clarity and consistency across the codebase. This aligns with the broader refactoring effort to standardize imports.

Backend/app/services/chat_services.py (1)

5-6: LGTM! Import path standardization looks good.

The change from relative to absolute imports using the app. prefix aligns with the broader import standardization across the codebase. This improves module resolution consistency without affecting functionality.

Frontend/vite.config.ts (1)

14-18: LGTM! Proxy configuration for API development is well-implemented.

The proxy setup correctly forwards /api requests to the backend server running on port 8000. This is a standard and necessary configuration for seamless frontend-backend integration during development.

Frontend/src/utils/supabase.tsx (1)

6-10: Excellent defensive programming with clear error messaging.

The runtime validation for Supabase environment variables is a great improvement. It prevents silent failures and provides clear, actionable error messages when configuration is missing. This will help developers quickly identify and fix configuration issues.

Frontend/src/components/ProtectedRoute.tsx (1)

5-7: LGTM! Cleaner authentication logic with explicit boolean check.

The change from checking a user object to using an explicit isAuthenticated boolean is more semantically clear and aligns with enhanced authentication state management. This approach is less prone to edge cases and more explicit about the authentication state.

Backend/app/main.py (2)

3-8: LGTM: Import changes improve consistency

The conversion to relative imports for internal modules is a good practice that improves code organization and maintainability.


8-8: LGTM: New router modules enhance API functionality

The addition of routes.match and routes.ai modules extends the API with matching functionality and AI-powered trending niches, which aligns well with the PR objectives.

Also applies to: 14-14, 56-57

Backend/sql.txt (1)

2-5: LGTM: Security improvement by removing password hashes

Removing password_hash from the seed data is a good security practice that prevents storing potentially sensitive authentication data in version control. This aligns well with the Google authentication integration mentioned in the PR objectives.

Frontend/src/pages/DashboardPage.tsx (2)

27-27: LGTM: Proper authentication integration

Good implementation of extracting the user object from the auth context to enable personalized sponsorship matching.


183-183: LGTM: Safe user ID handling

The use of optional chaining (user?.id || "") properly handles cases where the user might be null or undefined, providing a safe fallback.

Frontend/src/index.css (5)

122-133: LGTM: Well-implemented gradient animation

The gradient keyframe animation is properly structured with smooth transitions for background position changes.


135-142: LGTM: Smooth float animation

The float animation provides a subtle vertical movement effect that's pleasant for UI elements.


144-151: LGTM: Effective glow animation

The glow animation creates a nice pulsating effect using box-shadow variations with appropriate opacity levels.


153-164: LGTM: Consistent utility class naming

The utility classes follow a clear naming pattern and have appropriate animation durations and easing functions.


166-181: LGTM: Impressive 3D text effect

The layered text-shadow approach creates a convincing 3D depth effect. The progressive shadows and blur values are well-balanced.

Frontend/src/pages/Login.tsx (1)

33-33: Good architectural improvement with clear documentation.

Moving navigation logic to AuthContext improves separation of concerns and centralizes auth flow management. The comments clearly explain the delegation of responsibility.

Also applies to: 52-52

Backend/app/db/seed.py (2)

1-3: Good improvement to use absolute imports and add datetime support.

The changes to use absolute imports and add datetime functionality align well with the new fields being added.


15-16: Added new user fields appropriately.

The addition of profile_image and created_at fields with sensible defaults enhances the user data structure.

Also applies to: 25-26

README.md (2)

104-104: Excellent addition of explicit .env setup instructions.

Adding clear instructions to create .env files from examples improves the developer onboarding experience.

Also applies to: 133-133


270-270: Nice touch with the friendly closing message.

The "Happy Coding!" addition creates a welcoming tone for contributors.

Frontend/src/pages/Brand/Dashboard.tsx (2)

36-37: Good imports for the dynamic matching functionality.

The addition of useState and CreatorMatches component supports the transition from static to dynamic creator recommendations.


172-189: Excellent transition to dynamic, campaign-based creator matching.

The replacement of static creator cards with a dropdown selection and the CreatorMatches component creates a much more functional and user-friendly interface. The integration with the backend matching API (referenced in the relevant code snippet) provides real value to brands.

Backend/app/models/models.py (1)

12-16: LGTM on the import additions.

The new imports for TIMESTAMP and datetime are appropriate for the timestamp handling changes, and the absolute import path for Base is a good practice.

Frontend/src/App.tsx (1)

50-76: Excellent route protection implementation.

The use of PublicRoute and ProtectedRoute components provides a clean and secure way to manage access control. The new onboarding routes are well-structured and integrate properly with the authentication flow.

Frontend/src/pages/RoleSelection.tsx (1)

1-59: Excellent implementation of role selection component.

The component demonstrates good practices with proper state management, error handling, and user experience design. The validation ensures users must make a selection before proceeding, and the responsive UI with dark mode support is well-implemented.

Frontend/src/components/dashboard/creator-matches.tsx (1)

64-64: Verify the match score calculation logic

The match score is divided by 4 before converting to percentage. Please verify this calculation aligns with the backend scoring system.

Is the backend match score on a scale of 0-4? If not, this calculation may show incorrect percentages to users.

Frontend/src/components/dashboard/sponsorship-matches.tsx (1)

62-62: Ensure consistent match score calculation across components

The match score calculation (dividing by 4) should be consistent with CreatorMatches component and the backend logic.

Please verify this matches the backend scoring scale and consider extracting this calculation to a shared utility function for consistency.

Frontend/src/components/user-nav.tsx (1)

34-48: Well-implemented avatar error handling

The avatar error handling with fallback logic is well implemented, providing a good user experience when avatar images fail to load.

Comment on lines +10 to +15
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the relative import structure to avoid import resolution issues.

The change to relative imports may cause issues with the package structure. Static analysis tools are flagging these as "relative imports beyond top-level package."

Please verify the current package structure and ensure these relative imports work correctly in your deployment environment:

Consider reverting to absolute imports if the relative imports cause issues in your deployment environment:

-from ..db.db import get_db
-from ..services.chat_services import chat_service
-from ..services.redis_client import get_redis
-from ..services.chat_pubsub import listen_to_channel
+from app.db.db import get_db
+from app.services.chat_services import chat_service
+from app.services.redis_client import get_redis
+from app.services.chat_pubsub import listen_to_channel

🏁 Script executed:

#!/bin/bash
# Description: Check the package structure and verify import paths

# Check the directory structure
echo "=== Backend directory structure ==="
find Backend -type f -name "*.py" | head -20

echo -e "\n=== Checking if the imported modules exist ==="
# Check if the imported modules exist at the expected paths
ls -la Backend/app/db/db.py
ls -la Backend/app/services/chat_services.py
ls -la Backend/app/services/redis_client.py
ls -la Backend/app/services/chat_pubsub.py

echo -e "\n=== Checking for __init__.py files ==="
# Check for __init__.py files that make directories Python packages
find Backend -name "__init__.py" -type f

Length of output: 1409


Ensure package initialization or switch to absolute imports to resolve E0402

Verified that all referenced modules exist under Backend/app, but no __init__.py files are present—so static analysis flags “relative imports beyond top-level package.” Choose one of the following fixes:

• Add empty __init__.py in each folder to turn them into proper packages:

  • Backend/app/
  • Backend/app/db/
  • Backend/app/routes/
  • Backend/app/services/

• Or update imports in Backend/app/routes/chat.py to use the app package root:

-from ..db.db import get_db
-from ..services.chat_services import chat_service
-from ..services.redis_client import get_redis
-from ..services.chat_pubsub import listen_to_channel
+from app.db.db import get_db
+from app.services.chat_services import chat_service
+from app.services.redis_client import get_redis
+from app.services.chat_pubsub import listen_to_channel
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
from app.db.db import get_db
from app.services.chat_services import chat_service
from redis.asyncio import Redis
from app.services.redis_client import get_redis
import asyncio
from app.services.chat_pubsub import listen_to_channel
🧰 Tools
🪛 Pylint (3.3.7)

[error] 10-10: Attempted relative import beyond top-level package

(E0402)


[error] 11-11: Attempted relative import beyond top-level package

(E0402)


[error] 13-13: Attempted relative import beyond top-level package

(E0402)


[error] 15-15: Attempted relative import beyond top-level package

(E0402)

🤖 Prompt for AI Agents
In Backend/app/routes/chat.py around lines 10 to 15, the relative imports cause
E0402 errors because the directories lack __init__.py files. Fix this by adding
empty __init__.py files in Backend/app/, Backend/app/db/, Backend/app/routes/,
and Backend/app/services/ to make them proper packages, or alternatively, change
the imports to use absolute imports starting from the app package root.

Comment on lines +4 to 12
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
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unused imports to clean up the codebase.

The static analysis tools correctly identified that these imports are unused. This file uses Supabase client directly for database operations, making the SQLAlchemy imports (AsyncSessionLocal, model classes, and schema classes) unnecessary.

Remove the unused imports:

-from ..db.db import AsyncSessionLocal
-from ..models.models import (
-    User, AudienceInsights, Sponsorship, UserPost,
-    SponsorshipApplication, SponsorshipPayment, Collaboration
-)
-from ..schemas.schema import (
-    UserCreate, AudienceInsightsCreate, SponsorshipCreate, UserPostCreate,
-    SponsorshipApplicationCreate, SponsorshipPaymentCreate, CollaborationCreate
-)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
)
🧰 Tools
🪛 Ruff (0.11.9)

4-4: ..db.db.AsyncSessionLocal imported but unused

Remove unused import: ..db.db.AsyncSessionLocal

(F401)


6-6: ..models.models.User imported but unused

Remove unused import

(F401)


6-6: ..models.models.AudienceInsights imported but unused

Remove unused import

(F401)


6-6: ..models.models.Sponsorship imported but unused

Remove unused import

(F401)


6-6: ..models.models.UserPost imported but unused

Remove unused import

(F401)


7-7: ..models.models.SponsorshipApplication imported but unused

Remove unused import

(F401)


7-7: ..models.models.SponsorshipPayment imported but unused

Remove unused import

(F401)


7-7: ..models.models.Collaboration imported but unused

Remove unused import

(F401)

🪛 Flake8 (7.2.0)

[error] 4-4: '..db.db.AsyncSessionLocal' imported but unused

(F401)


[error] 5-5: '..models.models.User' imported but unused

(F401)


[error] 5-5: '..models.models.AudienceInsights' imported but unused

(F401)


[error] 5-5: '..models.models.Sponsorship' imported but unused

(F401)


[error] 5-5: '..models.models.UserPost' imported but unused

(F401)


[error] 5-5: '..models.models.SponsorshipApplication' imported but unused

(F401)


[error] 5-5: '..models.models.SponsorshipPayment' imported but unused

(F401)


[error] 5-5: '..models.models.Collaboration' imported but unused

(F401)

🪛 Pylint (3.3.7)

[error] 4-4: Attempted relative import beyond top-level package

(E0402)


[error] 5-8: Attempted relative import beyond top-level package

(E0402)


[error] 9-12: Attempted relative import beyond top-level package

(E0402)

🤖 Prompt for AI Agents
In Backend/app/routes/post.py around lines 4 to 12, remove all imports related
to AsyncSessionLocal, model classes (User, AudienceInsights, Sponsorship,
UserPost, SponsorshipApplication, SponsorshipPayment, Collaboration), and schema
classes (UserCreate, AudienceInsightsCreate, SponsorshipCreate, UserPostCreate,
SponsorshipApplicationCreate, SponsorshipPaymentCreate, CollaborationCreate)
since they are not used in this file. This cleanup will keep the codebase tidy
and avoid unnecessary dependencies.

Comment on lines 3 to 9
export function MainNav() {
return (
<nav className="hidden md:flex gap-6">
<Link to="/features" className="text-sm font-medium transition-colors hover:text-primary">
Features
</Link>
<Link to="/pricing" className="text-sm font-medium transition-colors hover:text-primary">
Pricing
</Link>
<Link to="/about" className="text-sm font-medium transition-colors hover:text-primary">
About
</Link>
<Link to="/contact" className="text-sm font-medium transition-colors hover:text-primary">
Contact
</Link>
{/* Navigation items removed - keeping component for future use */}
</nav>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider component purpose and usage

The component now renders an empty navigation element. While the comment suggests it's kept for future use, consider whether:

  1. This component is still being imported and used elsewhere
  2. An empty nav element provides any value to the UI
  3. It might be better to temporarily remove the component entirely or implement a placeholder

🏁 Script executed:

#!/bin/bash
# Description: Check where MainNav is imported and used
# Expected: Find imports and usages of MainNav component

echo "=== Searching for MainNav imports ==="
rg "import.*MainNav" --type tsx --type ts

echo -e "\n=== Searching for MainNav usage ==="
rg "MainNav" --type tsx --type ts -A 2 -B 2

Length of output: 315


Let’s retry searching without the --type flags, using globs instead:


🏁 Script executed:

#!/bin/bash
# Search for MainNav imports and usages using file globs

echo "=== Searching for MainNav imports ==="
rg "import.*MainNav" -g '*.tsx' -g '*.ts'

echo -e "\n=== Searching for JSX usage (<MainNav />) ==="
rg "<MainNav" -g '*.tsx' -g '*.ts'

Length of output: 554


Empty MainNav is still imported and rendered

After verifying, MainNav is imported in two places but only rendered in one—and that render produces an empty <nav>:

• Frontend/src/pages/BasicDetails.tsx
– Imported but never used
• Frontend/src/pages/HomePage.tsx
– Rendered twice (<MainNav />) but shows no navigation items

Recommendations:

  • Remove the unused import from BasicDetails.tsx
  • Decide how you want to handle the empty nav in HomePage.tsx:
    • Remove the <MainNav /> calls until actual menu items are added
    • Or update MainNav (Frontend/src/components/main-nav.tsx) with a placeholder (e.g. “Menu coming soon”) or real links
  • If you intend to re-enable navigation soon, consider gating MainNav behind a feature flag or clearly marking it as “under construction”

Paths to update:

  • Frontend/src/pages/BasicDetails.tsx
  • Frontend/src/pages/HomePage.tsx
  • Frontend/src/components/main-nav.tsx
🤖 Prompt for AI Agents
In Frontend/src/components/main-nav.tsx lines 3 to 9, the MainNav component
returns an empty nav element which is rendered twice in HomePage.tsx and
imported but unused in BasicDetails.tsx. To fix this, remove the unused MainNav
import from BasicDetails.tsx, then either remove the <MainNav /> calls in
HomePage.tsx until menu items are added or update MainNav to include a
placeholder message like "Menu coming soon" or actual navigation links.
Optionally, add a feature flag or a clear "under construction" comment in
MainNav to indicate its incomplete status.

Comment on lines +35 to +38
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix timezone inconsistency in timestamp handling.

The User model now uses timezone-naive datetime.utcnow() with TIMESTAMP columns, while other models in the same file still use timezone-aware datetime.now(timezone.utc) with DateTime(timezone=True). This inconsistency can cause issues when comparing timestamps across different models or when the database expects consistent timezone handling.

Consider one of these solutions:

Option 1 (Recommended): Use timezone-aware timestamps consistently

-from datetime import datetime
+from datetime import datetime, timezone
-    created_at = Column(TIMESTAMP, default=datetime.utcnow)
+    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
-    last_seen = Column(TIMESTAMP, default=datetime.utcnow)
+    last_seen = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))

Option 2: Update all models to use TIMESTAMP consistently

# Update all other DateTime(timezone=True) columns to use TIMESTAMP with datetime.utcnow
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
# at the top of the file, update the import
-from datetime import datetime
+from datetime import datetime, timezone
# in your User model, replace the naive TIMESTAMP fields
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
is_online = Column(Boolean, default=False) # ✅ Track if user is online
- last_seen = Column(TIMESTAMP, default=datetime.utcnow)
+ last_seen = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
🤖 Prompt for AI Agents
In Backend/app/models/models.py around lines 35 to 38, the timestamp columns use
timezone-naive datetime.utcnow() with TIMESTAMP, causing inconsistency with
other models that use timezone-aware datetime.now(timezone.utc) and
DateTime(timezone=True). To fix this, update the created_at and last_seen
columns to use DateTime(timezone=True) and set their default to a timezone-aware
function like datetime.now(timezone.utc) to ensure consistent timezone handling
across all models.

Comment on lines 1 to 1487
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronRight } from "lucide-react";
import { useAuth } from "../context/AuthContext";
import { Info } from "lucide-react";
import { supabase } from "../utils/supabase";

const platforms = [
{ name: "YouTube", icon: "/youtube.png" },
{ name: "Instagram", icon: "/instagram.png" },
{ name: "Facebook", icon: "/facebook.png" },
{ name: "TikTok", icon: "/tiktok.png" },
];

const steps = [
"Role Selection",
"Personal Details",
"Platform Selection",
"Platform Details",
"Pricing",
"Profile Picture",
"Review & Submit",
];

const YOUTUBE_API_KEY = import.meta.env.VITE_YOUTUBE_API_KEY;

type BrandData = {
brand_name: string;
logo: File | null;
website_url: string;
industry: string;
company_size: string;
location: string;
description: string;
contact_person: string;
contact_email: string;
contact_phone: string;
role: string;
platforms: string[];
social_links: Record<string, string>;
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<string[]>([]);
const [platformDetails, setPlatformDetails] = useState<any>({});
const [pricing, setPricing] = useState<any>({});
const [personalError, setPersonalError] = useState("");
const [platformDetailsError, setPlatformDetailsError] = useState("");
const [pricingError, setPricingError] = useState("");
const [profilePic, setProfilePic] = useState<File | null>(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<BrandData>(brandInitialState);
const [brandLogoPreview, setBrandLogoPreview] = useState<string | null>(null);
const [brandError, setBrandError] = useState("");

return (
<div className="bg-gray-200 flex flex-col items-center justify-center px-4 py-16">
<div className="max-w-md w-full text-center">
<h1 className="text-3xl font-bold text-gray-900">Welcome to Inpact</h1>
<p className="mt-2 text-gray-600">
Let's get you started with your profile setup
</p>
// 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 = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Are you a Brand or a Creator?</h2>
<div className="flex gap-8 justify-center">
<button
className={`flex flex-col items-center px-8 py-4 rounded-xl border-2 text-lg font-semibold transition-all duration-200 shadow-sm w-48 h-48 ${role === "brand" ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
onClick={() => setRole("brand")}
>
<img src="/brand.png" alt="Brand" className="h-20 w-20 mb-2" />
<span className="font-bold text-xl">Brand</span>
</button>
<button
className={`flex flex-col items-center px-8 py-4 rounded-xl border-2 text-lg font-semibold transition-all duration-200 shadow-sm w-48 h-48 ${role === "creator" ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
onClick={() => setRole("creator")}
>
<img src="/contnetcreator.png" alt="Content Creator" className="h-20 w-20 mb-2" />
<span className="font-bold text-xl">Content Creator</span>
</button>
</div>
</div>
);

// 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 = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Personal Details</h2>
<form className="grid grid-cols-1 gap-4 max-w-lg mx-auto">
<input
type="text"
placeholder="Name"
value={personal.name}
onChange={e => {
// 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
/>
<input
type="email"
placeholder="Email"
value={personal.email}
onChange={e => 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
/>
<input
type="number"
placeholder="Age"
value={personal.age}
onChange={e => {
// 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
/>
<select
value={personal.gender}
onChange={e => setPersonal({ ...personal, gender: 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
>
<option value="" disabled>
Select Gender
</option>
{genderOptions.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<select
value={personal.category}
onChange={e => setPersonal({ ...personal, category: 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
>
<option value="" disabled>
Select Content Category
</option>
{categoryOptions.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
{personal.category === "Other" && (
<input
type="text"
placeholder="Enter your content category"
value={personal.otherCategory || ""}
onChange={e => 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
/>
)}
<input
type="text"
placeholder="Country"
value={personal.country}
onChange={e => {
// 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
/>
</form>
{personalError && <div className="text-red-500 text-sm mt-2">{personalError}</div>}
</div>
);

<div className="mt-6 space-y-4">
{/* Influencer Button */}
// Step 3: Platform Selection
const renderPlatformStep = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Which platforms do you use?</h2>
<div className="flex flex-wrap gap-6 justify-center">
{platforms.map((platform) => (
<button
onClick={() => navigate("/signup")}
className="w-full flex items-center justify-between bg-purple-600 text-white font-medium px-6 py-3 rounded-lg shadow-md hover:bg-purple-700 transition"
key={platform.name}
type="button"
onClick={() => {
setSelectedPlatforms((prev) =>
prev.includes(platform.name)
? prev.filter((p) => p !== platform.name)
: [...prev, platform.name]
);
}}
className={`flex flex-col items-center px-6 py-4 rounded-xl border-2 transition-all duration-200 shadow-sm w-32 h-36 ${selectedPlatforms.includes(platform.name) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
>
I'm an Influencer
<ChevronRight className="h-5 w-5" />
<img src={platform.icon} alt={platform.name} className="h-12 w-12 mb-2" />
<span className="font-semibold text-lg">{platform.name}</span>
</button>
))}
</div>
</div>
);

// Step 4: Platform Details
const renderPlatformDetailsStep = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Platform Details</h2>
<div className="space-y-8">
{selectedPlatforms.map((platform) => (
<div key={platform} className="bg-gray-50 dark:bg-gray-700 rounded-xl p-6 shadow">
<div className="flex items-center gap-4 mb-4">
<img src={platforms.find(p => p.name === platform)?.icon} alt={platform} className="h-8 w-8" />
<span className="font-semibold text-lg">{platform}</span>
</div>
{platform === "YouTube" && (
<YouTubeDetails
details={platformDetails[platform] || {}}
setDetails={d => setPlatformDetails((prev: any) => ({ ...prev, [platform]: d }))}
/>
)}
{platform === "Instagram" && (
<InstagramDetails
details={platformDetails[platform] || {}}
setDetails={d => setPlatformDetails((prev: any) => ({ ...prev, [platform]: d }))}
/>
)}
{platform === "Facebook" && (
<FacebookDetails
details={platformDetails[platform] || {}}
setDetails={d => setPlatformDetails((prev: any) => ({ ...prev, [platform]: d }))}
/>
)}
{platform === "TikTok" && (
<TikTokDetails
details={platformDetails[platform] || {}}
setDetails={d => setPlatformDetails((prev: any) => ({ ...prev, [platform]: d }))}
/>
)}
</div>
))}
</div>
{platformDetailsError && <div className="text-red-500 text-sm mt-2">{platformDetailsError}</div>}
</div>
);

// Step 5: Pricing
const renderPricingStep = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Set Your Pricing</h2>
<div className="space-y-8">
{selectedPlatforms.map((platform) => (
<div key={platform} className="bg-gray-50 dark:bg-gray-700 rounded-xl p-6 shadow">
<div className="flex items-center gap-4 mb-4">
<img src={platforms.find(p => p.name === platform)?.icon} alt={platform} className="h-8 w-8" />
<span className="font-semibold text-lg">{platform}</span>
</div>
{platform === "YouTube" && (
<YouTubePricing
pricing={pricing[platform] || {}}
setPricing={d => setPricing((prev: any) => ({ ...prev, [platform]: d }))}
/>
)}
{platform === "Instagram" && (
<InstagramPricing
pricing={pricing[platform] || {}}
setPricing={d => setPricing((prev: any) => ({ ...prev, [platform]: d }))}
/>
)}
{platform === "Facebook" && (
<FacebookPricing
pricing={pricing[platform] || {}}
setPricing={d => setPricing((prev: any) => ({ ...prev, [platform]: d }))}
/>
)}
{platform === "TikTok" && (
<TikTokPricing
pricing={pricing[platform] || {}}
setPricing={d => setPricing((prev: any) => ({ ...prev, [platform]: d }))}
/>
)}
</div>
))}
</div>
{pricingError && <div className="text-red-500 text-sm mt-2">{pricingError}</div>}
</div>
);

// Step 5: Profile Picture Upload (new step)
const handleProfilePicChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Upload Profile Picture</h2>
<div className="flex flex-col items-start gap-2">
<label htmlFor="profile-pic-upload" className="inline-block px-6 py-2 bg-purple-600 text-white rounded-lg font-semibold cursor-pointer hover:bg-purple-700 transition-colors">
Choose File
</label>
<input
id="profile-pic-upload"
type="file"
accept="image/*"
onChange={handleProfilePicChange}
className="hidden"
/>
<div className="flex items-center gap-4 mt-2">
{(profilePic || user?.user_metadata?.avatar_url) ? (
<img
src={profilePic ? URL.createObjectURL(profilePic) : user?.user_metadata?.avatar_url}
alt="Profile Preview"
className="h-20 w-20 rounded-full object-cover border-2 border-purple-500"
/>
) : (
<div className="h-20 w-20 rounded-full bg-gray-200 flex items-center justify-center text-gray-400">No Image</div>
)}
{profilePic && <div className="text-sm">{profilePic.name}</div>}
</div>
{profilePicError && <div className="text-red-500 text-sm mt-2">{profilePicError}</div>}
<div className="text-xs text-gray-500 mt-2">Max file size: 3MB. You can skip this step if you want to use your Google/YouTube profile image.</div>
</div>
</div>
);

// 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 = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Review & Submit</h2>
{submitting && (
<div className="w-full bg-gray-200 rounded-full h-3 mb-4 overflow-hidden">
<div
className="bg-purple-600 h-3 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
></div>
</div>
)}
<div className="mb-4">
<label className="block font-medium mb-2">Profile Picture</label>
<div className="flex items-center gap-4">
{(profilePic || user?.user_metadata?.avatar_url) ? (
<img
src={profilePic ? URL.createObjectURL(profilePic) : user?.user_metadata?.avatar_url}
alt="Profile Preview"
className="h-20 w-20 rounded-full object-cover border-2 border-purple-500"
/>
) : (
<div className="h-20 w-20 rounded-full bg-gray-200 flex items-center justify-center text-gray-400">No Image</div>
)}
{profilePic && <div className="text-sm">{profilePic.name}</div>}
</div>
</div>
<div className="mb-4">
<h3 className="font-semibold">Personal Details</h3>
<ul className="text-sm">
<li><b>Name:</b> {personal.name}</li>
<li><b>Email:</b> {personal.email}</li>
<li><b>Age:</b> {personal.age}</li>
<li><b>Gender:</b> {personal.gender}</li>
<li><b>Country:</b> {personal.country}</li>
<li><b>Category:</b> {personal.category === 'Other' ? personal.otherCategory : personal.category}</li>
</ul>
</div>
<div className="mb-4">
<h3 className="font-semibold">Platforms</h3>
{selectedPlatforms.map(platform => (
<div key={platform} className="mb-2">
<b>{platform}</b>
<ul className="text-sm ml-4">
{platform === 'YouTube' ? (
<>
<li><b>Channel Name:</b> {platformDetails[platform]?.channelName}</li>
<li><b>Subscribers:</b> {platformDetails[platform]?.subscriber_count}</li>
<li><b>Videos:</b> {platformDetails[platform]?.video_count}</li>
<li><b>Views:</b> {platformDetails[platform]?.total_views}</li>
<li><b>Channel URL:</b> {platformDetails[platform]?.channelUrl}</li>
<li><b>Pricing:</b> Video: {pricing[platform]?.per_video_cost}, Short: {pricing[platform]?.per_short_cost}, Community Post: {pricing[platform]?.per_community_post_cost} ({pricing[platform]?.currency})</li>
</>
) : (
<>
<li><b>Profile URL:</b> {platformDetails[platform]?.profileUrl}</li>
<li><b>Followers:</b> {platformDetails[platform]?.followers}</li>
<li><b>Posts:</b> {platformDetails[platform]?.posts}</li>
<li><b>Pricing:</b> {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})</li>
</>
)}
</ul>
</div>
))}
</div>
{submitError && <div className="text-red-500 text-sm mb-2">{submitError}</div>}
{submitSuccess && <div className="text-green-600 text-sm mb-2">{submitSuccess}</div>}
</div>
);

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<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setBrandData({ ...brandData, logo: e.target.files[0] });
setBrandLogoPreview(URL.createObjectURL(e.target.files[0]));
}
};
const renderBrandDetailsStep = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Brand Details</h2>
<input
type="text"
placeholder="Brand Name"
value={brandData.brand_name}
onChange={e => setBrandData({ ...brandData, brand_name: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
/>
<label className="block font-medium mb-1">Logo (optional)</label>
<label htmlFor="brand-logo-upload" className="inline-block px-4 py-2 bg-purple-600 text-white rounded-lg font-semibold cursor-pointer hover:bg-purple-700 mb-2">Choose Logo</label>
<input id="brand-logo-upload" type="file" accept="image/*" onChange={handleBrandLogoChange} className="hidden" />
{brandLogoPreview && <img src={brandLogoPreview} alt="Logo Preview" className="h-16 w-16 rounded-full object-cover border mb-2" />}
<input
type="text"
placeholder="Website URL"
value={brandData.website_url}
onChange={e => setBrandData({ ...brandData, website_url: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
/>
<select
value={brandData.industry}
onChange={e => setBrandData({ ...brandData, industry: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
>
<option value="" disabled>Select Industry</option>
{industries.map(ind => <option key={ind} value={ind}>{ind}</option>)}
</select>
<select
value={brandData.company_size}
onChange={e => setBrandData({ ...brandData, company_size: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
>
<option value="" disabled>Select Company Size</option>
{companySizes.map(size => <option key={size} value={size}>{size}</option>)}
</select>
<input
type="text"
placeholder="Location"
value={brandData.location}
onChange={e => setBrandData({ ...brandData, location: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
/>
<textarea
placeholder="Description"
value={brandData.description}
onChange={e => setBrandData({ ...brandData, description: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
rows={3}
/>
</div>
);

// Brand Step 2: Contact Information
const renderBrandContactStep = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Contact Information</h2>
<input
type="text"
placeholder="Contact Person"
value={brandData.contact_person}
onChange={e => setBrandData({ ...brandData, contact_person: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
/>
<input
type="email"
placeholder="Contact Email"
value={brandData.contact_email}
onChange={e => setBrandData({ ...brandData, contact_email: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
/>
<input
type="text"
placeholder="Contact Phone"
value={brandData.contact_phone}
onChange={e => setBrandData({ ...brandData, contact_phone: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
/>
<input
type="text"
placeholder="Role at Company"
value={brandData.role}
onChange={e => setBrandData({ ...brandData, role: e.target.value })}
className="w-full px-4 py-3 rounded-lg border border-gray-300 mb-2"
/>
</div>
);

// Brand Step 3: Platforms
const allBrandPlatforms = [
{ name: "Instagram", key: "instagram_url" },
{ name: "YouTube", key: "youtube_url" },
{ name: "Facebook", key: "facebook_url" },
{ name: "Twitter", key: "twitter_url" },
{ name: "LinkedIn", key: "linkedin_url" },
// Add TikTok if needed
];
const renderBrandPlatformsStep = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Which platforms is your brand on?</h2>
<div className="flex flex-wrap gap-4">
{allBrandPlatforms.map(platform => (
<button
onClick={() => navigate("/signup")}
className="w-full flex items-center justify-between bg-purple-600 text-white font-medium px-6 py-3 rounded-lg shadow-md hover:bg-purple-700 transition"
key={platform.name}
type="button"
onClick={() => {
setBrandData(prev => {
const exists = prev.platforms.includes(platform.name);
return {
...prev,
platforms: exists
? prev.platforms.filter(p => p !== platform.name)
: [...prev.platforms, platform.name],
};
});
}}
className={`px-6 py-3 rounded-lg border-2 font-semibold ${brandData.platforms.includes(platform.name) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
>
I'm a Brand
<ChevronRight className="h-5 w-5" />
{platform.name}
</button>
))}
</div>
</div>
);

// Brand Step 4: Social Links (conditional)
const socialLinkExamples: Record<string, string> = {
instagram_url: "https://instagram.com/yourbrand",
youtube_url: "https://youtube.com/yourbrand",
facebook_url: "https://facebook.com/yourbrand",
twitter_url: "https://twitter.com/yourbrand",
linkedin_url: "https://linkedin.com/company/yourbrand",
};
const renderBrandSocialLinksStep = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Social Links</h2>
{brandData.platforms.map(platform => {
const key = allBrandPlatforms.find(p => p.name === platform)?.key;
if (!key) return null;
return (
<div key={platform} className="mb-2">
<label className="block font-medium mb-1">{platform} URL</label>
<input
type="text"
placeholder={socialLinkExamples[key] || `Enter your ${platform} URL`}
value={brandData.social_links[key] || ""}
onChange={e => setBrandData({
...brandData,
social_links: { ...brandData.social_links, [key]: e.target.value },
})}
className="w-full px-4 py-3 rounded-lg border border-gray-300"
/>
</div>
);
})}
</div>
);

// Brand Step 5: Collaboration Preferences
const collabTypes = ["Sponsored Posts", "Giveaways", "Product Reviews", "Long-term Partnerships", "Affiliate Marketing", "Events", "Content Creation", "Brand Ambassadorship", "Social Media Takeover", "Other"];
const creatorCategories = ["Tech", "Fashion", "Travel", "Food", "Fitness", "Beauty", "Gaming", "Education", "Music", "Finance", "Other"];
const brandValues = ["Sustainability", "Innovation", "Diversity", "Quality", "Community", "Transparency", "Customer Focus", "Creativity", "Integrity", "Other"];
const tones = ["Professional", "Friendly", "Humorous", "Inspirational", "Bold", "Casual", "Formal", "Playful", "Serious", "Other"];
const toggleMultiSelect = (field: keyof BrandData, value: string) => {
setBrandData(prev => {
const arr = prev[field] as string[];
return {
...prev,
[field]: arr.includes(value) ? arr.filter(v => v !== value) : [...arr, value],
};
});
};
const renderBrandCollabPrefsStep = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Collaboration Preferences</h2>
<div className="mb-4">
<label className="block font-medium mb-1">Collaboration Types</label>
<div className="flex flex-wrap gap-2">
{collabTypes.map(type => (
<button
key={type}
type="button"
onClick={() => toggleMultiSelect("collaboration_types", type)}
className={`px-4 py-2 rounded-lg border-2 text-sm ${brandData.collaboration_types.includes(type) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
>
{type}
</button>
))}
</div>
</div>
<div className="mb-4">
<label className="block font-medium mb-1">Preferred Creator Categories</label>
<div className="flex flex-wrap gap-2">
{creatorCategories.map(cat => (
<button
key={cat}
type="button"
onClick={() => toggleMultiSelect("preferred_creator_categories", cat)}
className={`px-4 py-2 rounded-lg border-2 text-sm ${brandData.preferred_creator_categories.includes(cat) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
>
{cat}
</button>
))}
</div>
</div>
<div className="mb-4">
<label className="block font-medium mb-1">Brand Values</label>
<div className="flex flex-wrap gap-2">
{brandValues.map(val => (
<button
key={val}
type="button"
onClick={() => toggleMultiSelect("brand_values", val)}
className={`px-4 py-2 rounded-lg border-2 text-sm ${brandData.brand_values.includes(val) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
>
{val}
</button>
))}
</div>
</div>
<div className="mb-4">
<label className="block font-medium mb-1">Preferred Tone</label>
<div className="flex flex-wrap gap-2">
{tones.map(tone => (
<button
key={tone}
type="button"
onClick={() => toggleMultiSelect("preferred_tone", tone)}
className={`px-4 py-2 rounded-lg border-2 text-sm ${brandData.preferred_tone.includes(tone) ? "border-purple-600 bg-purple-50" : "border-gray-300 bg-white"}`}
>
{tone}
</button>
))}
</div>
</div>
</div>
);

// Brand step validation
const validateBrandStep = () => {
if (brandStep === 0) {
if (!brandData.brand_name) return "Brand name is required.";
if (!brandData.website_url) return "Website URL is required.";
if (!brandData.industry) return "Industry is required.";
if (!brandData.company_size) return "Company size is required.";
if (!brandData.location) return "Location is required.";
if (!brandData.description) return "Description is required.";
}
if (brandStep === 1) {
if (!brandData.contact_person) return "Contact person is required.";
if (!brandData.contact_email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(brandData.contact_email)) return "Valid contact email is required.";
}
if (brandStep === 2) {
if (!brandData.platforms.length) return "Select at least one platform.";
}
if (brandStep === 3) {
for (const platform of brandData.platforms) {
const key = allBrandPlatforms.find(p => p.name === platform)?.key;
if (key && !brandData.social_links[key]) return `Enter your ${platform} URL.`;
if (key && brandData.social_links[key] && !/^https?:\/\//.test(brandData.social_links[key])) return `${platform} URL must start with http:// or https://`;
}
}
if (brandStep === 4) {
if (!brandData.collaboration_types.length) return "Select at least one collaboration type.";
if (!brandData.preferred_creator_categories.length) return "Select at least one creator category.";
if (!brandData.brand_values.length) return "Select at least one brand value.";
if (!brandData.preferred_tone.length) return "Select at least one preferred tone.";
}
return "";
};
const handleBrandNext = () => {
const err = validateBrandStep();
if (err) {
setBrandError(err);
return;
} else {
setBrandError("");
}
if (brandStep < brandSteps.length - 1) setBrandStep(brandStep + 1);
};
const handleBrandBack = () => {
if (brandStep > 0) setBrandStep(brandStep - 1);
};

// Brand Step 6: Review & Submit
const [brandSubmitting, setBrandSubmitting] = useState(false);
const [brandSubmitError, setBrandSubmitError] = useState("");
const [brandSubmitSuccess, setBrandSubmitSuccess] = useState("");
const handleBrandSubmit = async () => {
setBrandSubmitting(true);
setBrandSubmitError("");
setBrandSubmitSuccess("");
let logo_url = null;
try {
// 1. Upload logo if provided
if (brandData.logo) {
const fileExt = brandData.logo.name.split('.').pop();
const fileName = `${user?.id}_${Date.now()}.${fileExt}`;
const { data, error } = await supabase.storage.from('brand-logos').upload(fileName, brandData.logo);
if (error) throw error;
logo_url = supabase.storage.from('brand-logos').getPublicUrl(fileName).data.publicUrl;
}
// 2. Insert into brands table
const { error: brandError } = await supabase.from('brands').insert({
user_id: user?.id,
brand_name: brandData.brand_name,
logo_url,
website_url: brandData.website_url,
industry: brandData.industry,
company_size: brandData.company_size,
location: brandData.location,
description: brandData.description,
contact_person: brandData.contact_person,
contact_email: brandData.contact_email,
contact_phone: brandData.contact_phone,
role: brandData.role,
instagram_url: brandData.social_links.instagram_url || null,
facebook_url: brandData.social_links.facebook_url || null,
twitter_url: brandData.social_links.twitter_url || null,
linkedin_url: brandData.social_links.linkedin_url || null,
youtube_url: brandData.social_links.youtube_url || null,
collaboration_types: brandData.collaboration_types,
preferred_creator_categories: brandData.preferred_creator_categories,
brand_values: brandData.brand_values,
preferred_tone: brandData.preferred_tone,
platforms: brandData.platforms,
});
if (brandError) throw brandError;
setBrandSubmitSuccess("Brand onboarding complete! Redirecting to dashboard...");
// Clear localStorage for brand onboarding
localStorage.removeItem("brandStep");
localStorage.removeItem("brandData");
setTimeout(() => navigate("/brand/dashboard"), 1200);
} catch (err: any) {
setBrandSubmitError(err.message || "Failed to submit brand onboarding data.");
} finally {
setBrandSubmitting(false);
}
};
const renderBrandReviewStep = () => (
<div>
<h2 className="text-2xl font-bold mb-4">Review & Submit</h2>
<div className="mb-4">
<label className="block font-medium mb-2">Logo</label>
{(brandLogoPreview || brandData.logo) ? (
<img src={brandLogoPreview || (brandData.logo ? URL.createObjectURL(brandData.logo) : undefined)} alt="Logo Preview" className="h-16 w-16 rounded-full object-cover border mb-2" />
) : (
<div className="h-16 w-16 rounded-full bg-gray-200 flex items-center justify-center text-gray-400">No Logo</div>
)}
</div>
<div className="mb-4">
<h3 className="font-semibold">Brand Details</h3>
<ul className="text-sm">
<li><b>Name:</b> {brandData.brand_name}</li>
<li><b>Website:</b> {brandData.website_url}</li>
<li><b>Industry:</b> {brandData.industry}</li>
<li><b>Company Size:</b> {brandData.company_size}</li>
<li><b>Location:</b> {brandData.location}</li>
<li><b>Description:</b> {brandData.description}</li>
</ul>
</div>
<div className="mb-4">
<h3 className="font-semibold">Contact Information</h3>
<ul className="text-sm">
<li><b>Contact Person:</b> {brandData.contact_person}</li>
<li><b>Email:</b> {brandData.contact_email}</li>
<li><b>Phone:</b> {brandData.contact_phone}</li>
<li><b>Role:</b> {brandData.role}</li>
</ul>
</div>
<div className="mb-4">
<h3 className="font-semibold">Platforms & Social Links</h3>
<ul className="text-sm">
{brandData.platforms.map(platform => {
const key = allBrandPlatforms.find(p => p.name === platform)?.key;
return (
<li key={platform}><b>{platform}:</b> {key ? brandData.social_links[key] : ""}</li>
);
})}
</ul>
</div>
<div className="mb-4">
<h3 className="font-semibold">Collaboration Preferences</h3>
<ul className="text-sm">
<li><b>Collaboration Types:</b> {brandData.collaboration_types.join(", ")}</li>
<li><b>Preferred Creator Categories:</b> {brandData.preferred_creator_categories.join(", ")}</li>
<li><b>Brand Values:</b> {brandData.brand_values.join(", ")}</li>
<li><b>Preferred Tone:</b> {brandData.preferred_tone.join(", ")}</li>
</ul>
</div>
{brandSubmitError && <div className="text-red-500 text-sm mb-2">{brandSubmitError}</div>}
{brandSubmitSuccess && <div className="text-green-600 text-sm mb-2">{brandSubmitSuccess}</div>}
<button
type="button"
onClick={handleBrandSubmit}
disabled={brandSubmitting}
className="px-6 py-2 rounded-lg bg-green-600 text-white font-semibold disabled:opacity-50"
>
{brandSubmitting ? 'Submitting...' : 'Submit'}
</button>
</div>
);

// Persist and restore brand onboarding state
useEffect(() => {
const savedStep = localStorage.getItem("brandStep");
const savedData = localStorage.getItem("brandData");
if (savedStep) setBrandStep(Number(savedStep));
if (savedData) setBrandData(JSON.parse(savedData));
}, []);
useEffect(() => {
localStorage.setItem("brandStep", String(brandStep));
localStorage.setItem("brandData", JSON.stringify(brandData));
}, [brandStep, brandData]);

return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-br from-purple-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
<div className="w-full max-w-2xl bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
{/* Stepper UI */}
<div className="flex justify-between mb-8">
{role === "brand"
? brandSteps.map((label, idx) => (
<div key={label} className={`flex-1 text-center text-xs font-semibold ${idx === brandStep ? "text-purple-600" : "text-gray-400"}`}>{label}</div>
))
: steps.map((label, idx) => (
<div key={label} className={`flex-1 text-center text-xs font-semibold ${idx === step ? "text-purple-600" : "text-gray-400"}`}>{label}</div>
))}
</div>
{/* Step Content */}
<div className="mb-8">
{role === "brand" ? (
<>
{brandStep === 0 && renderBrandDetailsStep()}
{brandStep === 1 && renderBrandContactStep()}
{brandStep === 2 && renderBrandPlatformsStep()}
{brandStep === 3 && renderBrandSocialLinksStep()}
{brandStep === 4 && renderBrandCollabPrefsStep()}
{brandStep === 5 && renderBrandReviewStep()}
</>
) : (
<>
{step === 0 && renderRoleStep()}
{step === 1 && renderPersonalStep()}
{step === 2 && renderPlatformStep()}
{step === 3 && renderPlatformDetailsStep()}
{step === 4 && renderPricingStep()}
{step === 5 && renderProfilePicStep()}
{step === 6 && renderReviewStep()}
</>
)}
</div>
{/* Navigation */}
<div className="flex justify-between">
{role === "brand" ? (
<>
<button
onClick={handleBrandBack}
disabled={brandStep === 0}
className="px-6 py-2 rounded-lg bg-gray-200 text-gray-700 font-semibold disabled:opacity-50"
>
Back
</button>
{brandStep < brandSteps.length - 1 ? (
<button
onClick={handleBrandNext}
disabled={!!validateBrandStep()}
className="px-6 py-2 rounded-lg bg-purple-600 text-white font-semibold disabled:opacity-50"
>
Next
</button>
) : null}
</>
) : (
<>
<button
onClick={handleBack}
disabled={step === 0}
className="px-6 py-2 rounded-lg bg-gray-200 text-gray-700 font-semibold disabled:opacity-50"
>
Back
</button>
{step < steps.length - 1 ? (
<button
onClick={handleNext}
disabled={
(step === 0 && !role) ||
(step === 1 && !!validatePersonal()) ||
(step === 2 && selectedPlatforms.length === 0) ||
(step === 3 && !!validatePlatformDetails()) ||
(step === 4 && !!validatePricing()) ||
(step === 5 && !!profilePicError)
}
className="px-6 py-2 rounded-lg bg-purple-600 text-white font-semibold disabled:opacity-50"
>
Next
</button>
) : (
<button
onClick={handleSubmit}
disabled={submitting}
className="px-6 py-2 rounded-lg bg-green-600 text-white font-semibold disabled:opacity-50"
>
{submitting ? 'Submitting...' : 'Submit'}
</button>
)}
</>
)}
</div>
{brandError && <div className="text-red-500 text-sm mt-2">{brandError}</div>}
</div>
</div>
);
}

// Platform detail components
function YouTubeDetails({ details, setDetails }: { details: any, setDetails: (d: any) => void }) {
const [input, setInput] = useState(details.channelUrl || "");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [showInfo, setShowInfo] = useState(false);

const fetchChannel = async () => {
setLoading(true);
setError("");
let channelId = input;
// Extract channel ID from URL if needed
if (input.includes("youtube.com")) {
const match = input.match(/(?:channel\/|user\/|c\/)?([\w-]{21,})/);
if (match) channelId = match[1];
}
try {
const res = await fetch(
`https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&id=${channelId}&key=${YOUTUBE_API_KEY}`
);
const data = await res.json();
if (data.items && data.items.length > 0) {
const ch = data.items[0];
setDetails({
channelUrl: input,
channelId: ch.id,
channelName: ch.snippet.title,
profile_image: ch.snippet.thumbnails.default.url,
subscriber_count: ch.statistics.subscriberCount,
total_views: ch.statistics.viewCount,
video_count: ch.statistics.videoCount,
});
} else {
setError("Channel not found");
}
} catch (e) {
setError("Failed to fetch channel");
} finally {
setLoading(false);
}
};

return (
<div className="space-y-2">
<label className="block font-medium flex items-center gap-2">
YouTube Channel URL or ID
<button
type="button"
onClick={() => setShowInfo(true)}
className="ml-1 text-purple-600 hover:text-purple-800 focus:outline-none"
aria-label="How to find your YouTube channel URL or ID"
>
<Info className="h-4 w-4" />
</button>
</label>
<input
type="text"
value={input}
onChange={e => setInput(e.target.value)}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="e.g. https://www.youtube.com/channel/UCxxxxxxxxxxxxxxxxxxx or channel ID"
/>
{/* Info Dialog */}
{showInfo && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 max-w-md w-full relative">
<button
className="absolute top-2 right-2 text-gray-500 hover:text-gray-700"
onClick={() => setShowInfo(false)}
aria-label="Close"
>
×
</button>
<h3 className="text-lg font-bold mb-2">How to find your YouTube Channel URL or ID</h3>
<ol className="list-decimal list-inside space-y-2 text-sm text-gray-700 dark:text-gray-200">
<li>Go to <a href="https://www.youtube.com" target="_blank" rel="noopener noreferrer" className="text-purple-600 underline">youtube.com</a> and sign in.</li>
<li>Click your profile picture at the top right and select <b>Your Channel</b>.</li>
<li>Click <b>Customize Channel</b> (top right).</li>
<li>Go to the <b>Basic info</b> tab.</li>
<li>Find the <b>Channel URL</b> section and copy the URL shown there.</li>
<li>Paste the full Channel URL above (e.g. <span className="font-mono">https://www.youtube.com/channel/UCxxxxxxxxxxxxxxxxxxx</span>).</li>
</ol>
</div>
</div>
)}
<button
type="button"
onClick={fetchChannel}
className="px-4 py-2 bg-purple-600 text-white rounded"
disabled={loading || !input}
>
{loading ? "Fetching..." : "Fetch Channel"}
</button>
{error && <div className="text-red-500 text-sm">{error}</div>}
{details.channelName && (
<div className="mt-2 p-2 bg-gray-100 rounded">
<div><b>Name:</b> {details.channelName}</div>
<div><b>Subscribers:</b> {details.subscriber_count}</div>
<div><b>Videos:</b> {details.video_count}</div>
<div><b>Views:</b> {details.total_views}</div>
</div>
)}
</div>
);
}

function InstagramDetails({ details, setDetails }: { details: any, setDetails: (d: any) => void }) {
return (
<div className="space-y-2">
<label className="block font-medium">Instagram Profile URL</label>
<input
type="text"
value={details.profileUrl || ""}
onChange={e => setDetails({ ...details, profileUrl: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Paste your Instagram profile URL"
/>
<label className="block font-medium">Followers</label>
<input
type="number"
value={details.followers || ""}
onChange={e => setDetails({ ...details, followers: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Followers count"
/>
<label className="block font-medium">Posts</label>
<input
type="number"
value={details.posts || ""}
onChange={e => setDetails({ ...details, posts: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Number of posts"
/>
</div>
);
}

function FacebookDetails({ details, setDetails }: { details: any, setDetails: (d: any) => void }) {
return (
<div className="space-y-2">
<label className="block font-medium">Facebook Profile URL</label>
<input
type="text"
value={details.profileUrl || ""}
onChange={e => setDetails({ ...details, profileUrl: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Paste your Facebook profile URL"
/>
<label className="block font-medium">Followers</label>
<input
type="number"
value={details.followers || ""}
onChange={e => setDetails({ ...details, followers: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Followers count"
/>
<label className="block font-medium">Posts</label>
<input
type="number"
value={details.posts || ""}
onChange={e => setDetails({ ...details, posts: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Number of posts"
/>
</div>
);
}

function TikTokDetails({ details, setDetails }: { details: any, setDetails: (d: any) => void }) {
return (
<div className="space-y-2">
<label className="block font-medium">TikTok Profile URL</label>
<input
type="text"
value={details.profileUrl || ""}
onChange={e => setDetails({ ...details, profileUrl: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Paste your TikTok profile URL"
/>
<label className="block font-medium">Followers</label>
<input
type="number"
value={details.followers || ""}
onChange={e => setDetails({ ...details, followers: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Followers count"
/>
<label className="block font-medium">Posts</label>
<input
type="number"
value={details.posts || ""}
onChange={e => setDetails({ ...details, posts: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Number of posts"
/>
</div>
);
}

// Pricing components
function YouTubePricing({ pricing, setPricing }: { pricing: any, setPricing: (d: any) => void }) {
return (
<div className="space-y-2">
<label className="block font-medium">Per Video</label>
<input
type="number"
value={pricing.per_video_cost || ""}
onChange={e => setPricing({ ...pricing, per_video_cost: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Price per video"
/>
<label className="block font-medium">Per Short</label>
<input
type="number"
value={pricing.per_short_cost || ""}
onChange={e => setPricing({ ...pricing, per_short_cost: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Price per short"
/>
<label className="block font-medium">Per Community Post</label>
<input
type="number"
value={pricing.per_community_post_cost || ""}
onChange={e => setPricing({ ...pricing, per_community_post_cost: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Price per community post"
/>
<label className="block font-medium">Currency</label>
<input
type="text"
value={pricing.currency || ""}
onChange={e => setPricing({ ...pricing, currency: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="e.g. USD, INR"
/>
</div>
);
}

function InstagramPricing({ pricing, setPricing }: { pricing: any, setPricing: (d: any) => void }) {
return (
<div className="space-y-2">
<label className="block font-medium">Per Post</label>
<input
type="number"
value={pricing.per_post_cost || ""}
onChange={e => setPricing({ ...pricing, per_post_cost: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Price per post"
/>
<label className="block font-medium">Per Story</label>
<input
type="number"
value={pricing.per_story_cost || ""}
onChange={e => setPricing({ ...pricing, per_story_cost: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Price per story"
/>
<label className="block font-medium">Per Reel</label>
<input
type="number"
value={pricing.per_reel_cost || ""}
onChange={e => setPricing({ ...pricing, per_reel_cost: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Price per reel"
/>
<label className="block font-medium">Currency</label>
<input
type="text"
value={pricing.currency || ""}
onChange={e => setPricing({ ...pricing, currency: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="e.g. USD, INR"
/>
</div>
);
}

function FacebookPricing({ pricing, setPricing }: { pricing: any, setPricing: (d: any) => void }) {
return (
<div className="space-y-2">
<label className="block font-medium">Per Post</label>
<input
type="number"
value={pricing.per_post_cost || ""}
onChange={e => setPricing({ ...pricing, per_post_cost: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Price per post"
/>
<label className="block font-medium">Currency</label>
<input
type="text"
value={pricing.currency || ""}
onChange={e => setPricing({ ...pricing, currency: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="e.g. USD, INR"
/>
</div>
);
}

function TikTokPricing({ pricing, setPricing }: { pricing: any, setPricing: (d: any) => void }) {
return (
<div className="space-y-2">
<label className="block font-medium">Per Video</label>
<input
type="number"
value={pricing.per_video_cost || ""}
onChange={e => setPricing({ ...pricing, per_video_cost: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="Price per video"
/>
<label className="block font-medium">Currency</label>
<input
type="text"
value={pricing.currency || ""}
onChange={e => setPricing({ ...pricing, currency: e.target.value })}
className="w-full px-4 py-2 rounded border border-gray-300"
placeholder="e.g. USD, INR"
/>
</div>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Break up this large component into smaller, manageable pieces.

This component is 1487 lines long, making it difficult to maintain and test. Consider splitting it into smaller components.

Suggested structure:

components/
  onboarding/
    Onboarding.tsx (main orchestrator)
    CreatorOnboarding.tsx
    BrandOnboarding.tsx
    steps/
      RoleSelection.tsx
      PersonalDetails.tsx
      PlatformSelection.tsx
      PlatformDetails.tsx
      Pricing.tsx
      ProfilePicture.tsx
      ReviewSubmit.tsx
    platform-components/
      YouTubeDetails.tsx
      InstagramDetails.tsx
      FacebookDetails.tsx
      TikTokDetails.tsx
    hooks/
      useOnboardingState.ts
      useValidation.ts
      useSubmission.ts
    utils/
      validation.ts
      api.ts

This would improve:

  • Code organization and maintainability
  • Testing capabilities
  • Reusability of components
  • Performance (smaller bundle sizes with code splitting)

Would you like me to help create this modular structure?

🧰 Tools
🪛 Biome (1.9.4)

[error] 392-392: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 645-645: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🤖 Prompt for AI Agents
In Frontend/src/components/Onboarding.tsx lines 1 to 1487, the component is
excessively large and complex, making it hard to maintain and test. To fix this,
refactor by splitting the file into smaller, focused components and hooks as
suggested: create separate files for main orchestrator (Onboarding.tsx),
CreatorOnboarding.tsx, BrandOnboarding.tsx, individual step components under
steps/, platform-specific components under platform-components/, and custom
hooks for state, validation, and submission logic under hooks/. Also, move
utility functions to utils/. This modular structure will improve code
organization, reusability, and testability.

Comment on lines +436 to +526
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);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and add cleanup on failure.

The submit handler uploads files before database operations. If database operations fail, uploaded files aren't cleaned up.

Consider this improved error handling approach:

 const handleSubmit = async () => {
   setSubmitting(true);
   setSubmitError("");
   setSubmitSuccess("");
   setProgress(0);
   let profile_image_url = null;
+  let uploadedFileName = 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}`;
+      uploadedFileName = fileName;
       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;
     }
     
     // ... rest of the code ...
     
   } catch (err: any) {
+    // Cleanup uploaded file on error
+    if (uploadedFileName) {
+      try {
+        await supabase.storage.from('profile-pictures').remove([uploadedFileName]);
+      } catch (cleanupError) {
+        console.error("Failed to cleanup uploaded file:", cleanupError);
+      }
+    }
     setSubmitError(err.message || 'Failed to submit onboarding data.');
     setProgress(0);
   } finally {
     setSubmitting(false);
   }
 };

Also consider wrapping all database operations in a transaction-like pattern or implementing a rollback mechanism for better data consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 handleSubmit = async () => {
setSubmitting(true);
setSubmitError("");
setSubmitSuccess("");
setProgress(0);
let profile_image_url = null;
let uploadedFileName = 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}`;
uploadedFileName = fileName;
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) {
// Cleanup uploaded file on error
if (uploadedFileName) {
try {
await supabase
.storage
.from('profile-pictures')
.remove([uploadedFileName]);
} catch (cleanupError) {
console.error("Failed to cleanup uploaded file:", cleanupError);
}
}
setSubmitError(err.message || 'Failed to submit onboarding data.');
setProgress(0);
} finally {
setSubmitting(false);
}
};
🤖 Prompt for AI Agents
In Frontend/src/components/Onboarding.tsx lines 436 to 526, the handleSubmit
function uploads a profile picture before performing database updates, but if
any database operation fails, the uploaded file is not deleted, causing
potential orphaned files. To fix this, add error handling that deletes the
uploaded profile picture from storage if subsequent database operations fail.
Additionally, consider grouping the database updates and inserts into a
transaction or simulate a rollback by deleting or reverting changes if any step
fails, ensuring data consistency. Implement cleanup logic in the catch block to
remove partial uploads and maintain a consistent state.

Comment on lines +273 to +286
fetch("/api/trending-niches")
.then(res => {
if (!res.ok) throw new Error("Failed to fetch trending niches");
return res.json();
})
.then(data => {
setNiches(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and add retry mechanism for API calls.

The current error handling is basic and doesn't provide a good user experience when the API fails.

Consider adding a retry mechanism and better error UI:

 useEffect(() => {
+  let retryCount = 0;
+  const maxRetries = 3;
+  
+  const fetchNiches = async () => {
+    try {
+      const res = await fetch("/api/trending-niches");
+      if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);
+      const data = await res.json();
+      setNiches(data);
+      setLoading(false);
+    } catch (err) {
+      if (retryCount < maxRetries) {
+        retryCount++;
+        setTimeout(() => fetchNiches(), 1000 * retryCount); // Exponential backoff
+      } else {
+        setError(err.message || "Failed to load trending niches");
+        setLoading(false);
+      }
+    }
+  };
+  
+  fetchNiches();
-  fetch("/api/trending-niches")
-    .then(res => {
-      if (!res.ok) throw new Error("Failed to fetch trending niches");
-      return res.json();
-    })
-    .then(data => {
-      setNiches(data);
-      setLoading(false);
-    })
-    .catch(err => {
-      setError(err.message);
-      setLoading(false);
-    });
 }, []);

Also improve the error UI:

-if (error) return <div>Error: {error}</div>;
+if (error) return (
+  <div className="text-center p-8">
+    <p className="text-red-600 mb-4">Unable to load trending niches</p>
+    <button 
+      onClick={() => window.location.reload()} 
+      className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
+    >
+      Try Again
+    </button>
+  </div>
+);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fetch("/api/trending-niches")
.then(res => {
if (!res.ok) throw new Error("Failed to fetch trending niches");
return res.json();
})
.then(data => {
setNiches(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
// Replace the existing fetch logic in useEffect
useEffect(() => {
let retryCount = 0;
const maxRetries = 3;
const fetchNiches = async () => {
try {
const res = await fetch("/api/trending-niches");
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);
const data = await res.json();
setNiches(data);
setLoading(false);
} catch (err) {
if (retryCount < maxRetries) {
retryCount++;
setTimeout(() => fetchNiches(), 1000 * retryCount); // Exponential backoff
} else {
setError(err.message || "Failed to load trending niches");
setLoading(false);
}
}
};
fetchNiches();
}, []);
// Replace the simple error line with a richer UI
if (error) return (
<div className="text-center p-8">
<p className="text-red-600 mb-4">Unable to load trending niches</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Try Again
</button>
</div>
);
🤖 Prompt for AI Agents
In Frontend/src/pages/HomePage.tsx around lines 273 to 286, the fetch call for
trending niches has basic error handling without retries or enhanced user
feedback. Implement a retry mechanism that attempts the fetch multiple times
before failing, with delays between retries. Additionally, improve the error UI
by providing more informative messages or visual cues to the user when an error
occurs. Update the state management to reflect loading, success, and error
states clearly during retries.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
Backend/app/routes/ai.py (1)

32-36: Raw Gemini response logged → sensitive data leakage
This exact issue was raised in a previous review and is still present. Please remove or sanitise these print statements and use the logging module at an appropriate level.

Suggested minimal change:

-import logging
-...
-print("Gemini raw response:", resp.text)
-...
-print("Gemini parsed JSON:", data)
-...
-print("Gemini text to parse as JSON:", text)
+import logging
+logger = logging.getLogger(__name__)
+...
+logger.debug("Gemini API call succeeded")
🧹 Nitpick comments (2)
Backend/app/routes/ai.py (2)

18-20: Avoid crashing the whole app at import-time

Raising a ValueError when env-vars are missing will stop the entire FastAPI process from starting, even for endpoints that don’t depend on Gemini/Supabase. Prefer validating inside a startup event-handler or within the route itself so the service can still come up (e.g. health-checks) and return a 500 only for the affected endpoint.

Example:

-# 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")
+def _validate_env():
+    missing = [k for k, v in {
+        "SUPABASE_URL": SUPABASE_URL,
+        "SUPABASE_KEY": SUPABASE_KEY,
+        "GEMINI_API_KEY": GEMINI_API_KEY,
+    }.items() if not v]
+    if missing:
+        raise RuntimeError(f"Missing required env vars: {', '.join(missing)}")

…and call _validate_env() inside fetch_from_gemini() or a @app.on_event("startup") handler.


57-65: Inefficient per-row inserts – switch to bulk upsert

Inserting inside a loop issues 6 separate HTTP calls to Supabase. Use a single bulk insert() with the list, or an upsert() to avoid duplicates and cut latency by >80 %.

-            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()
+            payload = [
+                {
+                    "name": n["name"],
+                    "insight": n["insight"],
+                    "global_activity": int(n["global_activity"]),
+                    "fetched_at": today,
+                }
+                for n in niches
+            ]
+            supabase.table("trending_niches").insert(payload).execute()
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0eb0b1d and a9c06cf.

📒 Files selected for processing (1)
  • Backend/app/routes/ai.py (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
Backend/app/routes/ai.py (1)
Learnt from: muntaxir4
PR: AOSSIE-Org/InPactAI#56
File: Backend/app/services/redis_client.py:1-4
Timestamp: 2025-05-07T21:28:06.358Z
Learning: Hardcoded Redis connection parameters in Backend/app/services/redis_client.py are intentional during development, with plans to implement environment variable configuration later during production preparation.
🪛 Flake8 (7.2.0)
Backend/app/routes/ai.py

[error] 23-23: expected 2 blank lines, found 1

(E302)


[error] 26-26: too many blank lines (2)

(E303)


[error] 44-44: expected 2 blank lines, found 1

(E302)

Comment on lines +35 to +42
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unprotected JSON parse may explode on malformed output

If Gemini returns non-JSON or extra prose, json.loads will raise and the entire request falls into the broad except block. Wrap parsing in a dedicated try/except ValueError and surface a 502/500 with context instead of silent fallback.

-    return json.loads(text)
+    try:
+        return json.loads(text)
+    except ValueError as exc:
+        logger.warning("Gemini returned non-JSON payload: %s", text[:120])
+        raise RuntimeError("Gemini payload parsing failed") from exc

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Backend/app/routes/ai.py around lines 35 to 42, the call to json.loads is
unprotected and will raise an exception if the text is not valid JSON, causing
the entire request to fall into a broad except block. Wrap the json.loads call
in a try/except block that catches ValueError, and in the except block, return
or raise a 502 or 500 error with a clear message indicating JSON parsing failed,
so the error is surfaced with context instead of silently falling back.

@Saahi30
Copy link
Collaborator Author

Saahi30 commented Jul 4, 2025

@chandansgowda kindly review and merge

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
Backend/app/routes/post.py (1)

4-12: Remove unused imports to clean up the codebase.

The static analysis tools correctly identified that these imports are unused. This file uses Supabase client directly for database operations, making the SQLAlchemy imports unnecessary.

🧹 Nitpick comments (1)
Backend/app/routes/post.py (1)

14-14: Remove duplicate FastAPI import.

Line 14 imports APIRouter and HTTPException from FastAPI, but these are already imported at line 1. This creates a duplicate import that should be removed.

-from fastapi import APIRouter, HTTPException
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9c06cf and dcb9ab2.

📒 Files selected for processing (2)
  • Backend/app/models/models.py (2 hunks)
  • Backend/app/routes/post.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Backend/app/models/models.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
Backend/app/routes/post.py (1)
Backend/app/models/models.py (6)
  • User (25-53)
  • AudienceInsights (57-72)
  • Sponsorship (76-92)
  • UserPost (96-110)
  • SponsorshipApplication (114-128)
  • SponsorshipPayment (146-162)
🪛 Ruff (0.11.9)
Backend/app/routes/post.py

4-4: ..db.db.AsyncSessionLocal imported but unused

Remove unused import: ..db.db.AsyncSessionLocal

(F401)


6-6: ..models.models.User imported but unused

Remove unused import

(F401)


6-6: ..models.models.AudienceInsights imported but unused

Remove unused import

(F401)


6-6: ..models.models.Sponsorship imported but unused

Remove unused import

(F401)


6-6: ..models.models.UserPost imported but unused

Remove unused import

(F401)


7-7: ..models.models.SponsorshipApplication imported but unused

Remove unused import

(F401)


7-7: ..models.models.SponsorshipPayment imported but unused

Remove unused import

(F401)


7-7: ..models.models.Collaboration imported but unused

Remove unused import

(F401)

🪛 Flake8 (7.2.0)
Backend/app/routes/post.py

[error] 4-4: '..db.db.AsyncSessionLocal' imported but unused

(F401)


[error] 5-5: '..models.models.User' imported but unused

(F401)


[error] 5-5: '..models.models.AudienceInsights' imported but unused

(F401)


[error] 5-5: '..models.models.Sponsorship' imported but unused

(F401)


[error] 5-5: '..models.models.UserPost' imported but unused

(F401)


[error] 5-5: '..models.models.SponsorshipApplication' imported but unused

(F401)


[error] 5-5: '..models.models.SponsorshipPayment' imported but unused

(F401)


[error] 5-5: '..models.models.Collaboration' imported but unused

(F401)

🪛 Pylint (3.3.7)
Backend/app/routes/post.py

[error] 4-4: Attempted relative import beyond top-level package

(E0402)


[error] 5-8: Attempted relative import beyond top-level package

(E0402)

🔇 Additional comments (1)
Backend/app/routes/post.py (1)

38-53: Authentication refactor correctly implemented.

The removal of password_hash from the user creation is correct and aligns with the authentication refactor where Supabase handles authentication externally. The user creation logic properly inserts all necessary fields while delegating authentication to Supabase.

…Tube channel fetch error handling, add Gemini API timeout and retry, and various DRY and security improvements
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (6)
Backend/app/routes/match.py (1)

2-4: Clean up unused imports and commented code.

Since the Supabase client initialization is handled in the service layer, these imports and commented code are no longer needed.

-from supabase import create_client, Client
-import os
-from dotenv import load_dotenv
-
-# Load environment variables
-# load_dotenv()
-# url: str = os.getenv("SUPABASE_URL")
-# key: str = os.getenv("SUPABASE_KEY")
-# supabase: Client = create_client(url, key)

Also applies to: 7-11

Backend/app/routes/ai.py (2)

43-43: Remove debug statements that could expose sensitive data.

These print statements could log sensitive API responses in production environments.

-    print("Gemini raw response:", resp.text)
+    # Log success without exposing sensitive data
+    print("Gemini API request successful")
     data = resp.json()
-    print("Gemini parsed JSON:", data)
     text = data['candidates'][0]['content']['parts'][0]['text']
-    print("Gemini text to parse as JSON:", text)

Also applies to: 45-45, 47-47


53-53: Add explicit error handling for JSON parsing.

The JSON parsing could fail if Gemini returns malformed data.

-    return json.loads(text)
+    try:
+        return json.loads(text)
+    except ValueError as exc:
+        print(f"Warning: Gemini returned non-JSON payload: {text[:120]}")
+        raise RuntimeError("Failed to parse Gemini response as JSON") from exc
Frontend/src/components/Onboarding.tsx (2)

1-1496: Consider breaking this large component into smaller, focused modules.

At 1487 lines, this component is extremely difficult to maintain, test, and debug. While the functionality is comprehensive, the size creates significant technical debt.

Consider refactoring into a modular structure:

  • Separate components for each onboarding step
  • Custom hooks for state management and validation
  • Utility functions for API calls and data processing
  • Platform-specific components in dedicated files

This would improve code organization, reusability, and testability significantly.


436-526: Implement cleanup for failed submissions.

The file upload occurs before database operations, but uploaded files aren't cleaned up if database operations fail, potentially creating orphaned files.

 const handleSubmit = async () => {
   setSubmitting(true);
   setSubmitError("");
   setSubmitSuccess("");
   setProgress(0);
   let profile_image_url = null;
+  let uploadedFileName = 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}`;
+      uploadedFileName = fileName;
       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}`;
     }
     // ... rest of database operations ...
   } catch (err: any) {
+    // Cleanup uploaded file on error
+    if (uploadedFileName) {
+      try {
+        await supabase.storage.from('profile-pictures').remove([uploadedFileName]);
+      } catch (cleanupError) {
+        console.error("Failed to cleanup uploaded file:", cleanupError);
+      }
+    }
     setSubmitError(err.message || 'Failed to submit onboarding data.');
     setProgress(0);
   } finally {
     setSubmitting(false);
   }
 };
Frontend/src/pages/HomePage.tsx (1)

273-286: The API error handling still needs improvement as previously identified.

This is the same issue identified in previous reviews regarding basic error handling without retries or enhanced user feedback.

The fetch call still lacks a retry mechanism and proper error UI. Please refer to the previous review comment for the detailed implementation suggestions.

🧹 Nitpick comments (7)
Backend/app/main.py (1)

10-11: Remove unused imports to clean up the code.

The logging and os imports are not used anywhere in this file.

-import logging
-import os
Backend/app/routes/match.py (1)

15-27: Add proper spacing and improve error messages.

The endpoint logic is correct, but formatting and error messages could be improved.

+
 @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.")
+        raise HTTPException(status_code=404, detail="No matching creators found for this sponsorship.")
     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.")
+        raise HTTPException(status_code=404, detail="No matching sponsorship opportunities found for this creator.")
     return {"matches": matches}
-
-# Placeholder for endpoints, logic to be added next 
Frontend/src/components/Onboarding.tsx (1)

392-392: Consider using optional chaining for safer property access.

These property access patterns could benefit from optional chaining to prevent potential runtime errors.

Examples of improvements:

-if (e.target.files && e.target.files[0]) {
+if (e.target.files?.[0]) {

-if (errData && errData.detail) errMsg = errData.detail;
+errMsg = errData?.detail || errMsg;

Also applies to: 645-645, 1189-1189

Frontend/src/pages/HomePage.tsx (4)

34-262: Consider extracting data constants to separate modules for better maintainability.

The file contains several large constant arrays that make it quite lengthy. These could be moved to separate data files or potentially fetched from backend APIs for dynamic content.

Consider creating separate files:

+// Create src/data/features.ts
+export const features = [
+  // ... existing features data
+];

+// Create src/data/mockData.ts  
+export const successStories = [
+  // ... existing success stories data
+];
+export const brandShowcase = [
+  // ... existing brand showcase data  
+];

Then import them:

+import { features } from '../data/features';
+import { successStories, brandShowcase } from '../data/mockData';
-const features = [
-  // ... remove from here
-];

378-442: Consider extracting intersection observer logic into custom hooks.

The component manages multiple intersection observers which could be abstracted into reusable custom hooks for better maintainability and testability.

Create custom hooks:

// Create src/hooks/useIntersectionObserver.ts
function useIntersectionObserver(threshold = 0.1) {
  const [isVisible, setIsVisible] = useState(false);
  const [hasAnimated, setHasAnimated] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        const [entry] = entries;
        if (entry.isIntersecting && !hasAnimated) {
          setIsVisible(true);
          setHasAnimated(true);
        }
      },
      { threshold }
    );

    if (ref.current) observer.observe(ref.current);
    return () => {
      if (ref.current) observer.unobserve(ref.current);
    };
  }, [hasAnimated, threshold]);

  return { ref, isVisible };
}

Then use it in the component:

-const [isTrendingVisible, setIsTrendingVisible] = useState(false);
-const [hasAnimatedTrending, setHasAnimatedTrending] = useState(false);
+const { ref: trendingRef, isVisible: isTrendingVisible } = useIntersectionObserver();

445-694: Consider splitting authenticated and non-authenticated layouts into separate components.

The component is quite large and handles two distinct user experiences. Splitting these into separate components would improve maintainability and testing.

Extract into separate components:

// Create AuthenticatedHomePage.tsx
function AuthenticatedHomePage({ user }: { user: User }) {
  // ... authenticated user layout
}

// Create UnauthenticatedHomePage.tsx  
function UnauthenticatedHomePage() {
  // ... non-authenticated user layout
}

// Then in HomePage.tsx:
export default function HomePage() {
  const { isAuthenticated, user } = useAuth();
  
  if (isAuthenticated && user) {
    return <AuthenticatedHomePage user={user} />;
  }
  
  return <UnauthenticatedHomePage />;
}

497-498: Consider replacing inline styles with CSS classes for animation delays.

Inline styles with animationDelay could be moved to CSS classes for better maintainability.

Create CSS classes:

/* Add to your CSS file */
.animate-bounce-delay-500 {
  animation-delay: 0.5s;
}

.animate-bounce-delay-1000 {
  animation-delay: 1s;
}

Then replace:

-<div className="absolute -bottom-4 -left-4 w-12 h-12 bg-gradient-to-br from-purple-400 to-pink-500 rounded-full flex items-center justify-center shadow-lg animate-bounce" style={{animationDelay: '0.5s'}}>
+<div className="absolute -bottom-4 -left-4 w-12 h-12 bg-gradient-to-br from-purple-400 to-pink-500 rounded-full flex items-center justify-center shadow-lg animate-bounce animate-bounce-delay-500">

Also applies to: 815-816

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcb9ab2 and 9023356.

📒 Files selected for processing (8)
  • Backend/.env-example (1 hunks)
  • Backend/app/main.py (2 hunks)
  • Backend/app/routes/ai.py (1 hunks)
  • Backend/app/routes/match.py (1 hunks)
  • Frontend/src/components/Onboarding.tsx (1 hunks)
  • Frontend/src/components/main-nav.tsx (1 hunks)
  • Frontend/src/pages/BasicDetails.tsx (0 hunks)
  • Frontend/src/pages/HomePage.tsx (2 hunks)
💤 Files with no reviewable changes (1)
  • Frontend/src/pages/BasicDetails.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • Backend/.env-example
  • Frontend/src/components/main-nav.tsx
🧰 Additional context used
🧠 Learnings (2)
Backend/app/routes/ai.py (1)
Learnt from: muntaxir4
PR: AOSSIE-Org/InPactAI#56
File: Backend/app/services/redis_client.py:1-4
Timestamp: 2025-05-07T21:28:06.358Z
Learning: Hardcoded Redis connection parameters in Backend/app/services/redis_client.py are intentional during development, with plans to implement environment variable configuration later during production preparation.
Backend/app/routes/match.py (1)
Learnt from: muntaxir4
PR: AOSSIE-Org/InPactAI#56
File: Backend/app/services/redis_client.py:1-4
Timestamp: 2025-05-07T21:28:06.358Z
Learning: Hardcoded Redis connection parameters in Backend/app/services/redis_client.py are intentional during development, with plans to implement environment variable configuration later during production preparation.
🧬 Code Graph Analysis (3)
Frontend/src/components/Onboarding.tsx (2)
Frontend/src/context/AuthContext.tsx (1)
  • useAuth (216-222)
Frontend/src/utils/supabase.tsx (1)
  • supabase (11-11)
Backend/app/routes/ai.py (1)
Frontend/src/utils/supabase.tsx (1)
  • supabase (11-11)
Backend/app/routes/match.py (1)
Backend/app/services/db_service.py (2)
  • match_creators_for_brand (13-48)
  • match_brands_for_creator (51-85)
🪛 Biome (1.9.4)
Frontend/src/components/Onboarding.tsx

[error] 392-392: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 645-645: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 1189-1189: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🪛 Ruff (0.11.9)
Backend/app/main.py

10-10: logging imported but unused

Remove unused import: logging

(F401)


11-11: os imported but unused

Remove unused import: os

(F401)

Backend/app/routes/ai.py

101-101: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

Backend/app/routes/match.py

2-2: supabase.create_client imported but unused

Remove unused import

(F401)


2-2: supabase.Client imported but unused

Remove unused import

(F401)


3-3: os imported but unused

Remove unused import: os

(F401)


4-4: dotenv.load_dotenv imported but unused

Remove unused import: dotenv.load_dotenv

(F401)

🪛 Flake8 (7.2.0)
Backend/app/main.py

[error] 10-10: 'logging' imported but unused

(F401)


[error] 11-11: 'os' imported but unused

(F401)

Backend/app/routes/ai.py

[error] 25-25: expected 2 blank lines, found 1

(E302)


[error] 55-55: expected 2 blank lines, found 1

(E302)


[error] 84-84: expected 2 blank lines after class or function definition, found 1

(E305)


[error] 86-86: expected 2 blank lines, found 1

(E302)

Backend/app/routes/match.py

[error] 2-2: 'supabase.create_client' imported but unused

(F401)


[error] 2-2: 'supabase.Client' imported but unused

(F401)


[error] 3-3: 'os' imported but unused

(F401)


[error] 4-4: 'dotenv.load_dotenv' imported but unused

(F401)


[error] 15-15: expected 2 blank lines, found 1

(E302)


[error] 22-22: expected 2 blank lines, found 1

(E302)

🪛 Pylint (3.3.7)
Backend/app/routes/match.py

[error] 5-5: Attempted relative import beyond top-level package

(E0402)

🔇 Additional comments (7)
Backend/app/main.py (1)

3-8: LGTM! Router integration looks correct.

The new route imports and router includes are properly implemented to expose the matching and AI endpoints.

Also applies to: 14-14, 56-58

Backend/app/routes/ai.py (3)

19-21: Excellent addition of environment variable validation.

This prevents runtime errors from missing configuration and provides clear feedback.


30-41: Great implementation of retry strategy.

The retry logic with exponential backoff and proper status codes will improve reliability for external API calls.


86-101: YouTube proxy endpoint looks secure and well-implemented.

The API key is properly protected on the backend and error handling is appropriate.

Frontend/src/components/Onboarding.tsx (2)

24-24: Good security improvement removing frontend API key exposure.

Commenting out the YouTube API key and using the backend proxy is the correct approach for security.


1182-1214: YouTube channel fetching logic is well-implemented.

The backend proxy usage, error handling, and channel data extraction are all properly implemented for security and functionality.

Frontend/src/pages/HomePage.tsx (1)

337-376: Excellent refactoring to address code duplication.

Good work extracting the duplicate "Why Choose Inpact AI" section into a reusable component. This properly addresses the DRY principle violation identified in previous reviews.

Comment on lines +804 to +808
<img
src="/Home.png"
alt="Hero Image"
className="rounded-xl object-cover w-full h-auto shadow-lg"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same image accessibility issue applies here.

This image also needs proper alt text and error handling.

Apply the same improvements as suggested for the authenticated version:

-<img
-  src="/Home.png"
-  alt="Hero Image"
-  className="rounded-xl object-cover w-full h-auto shadow-lg"
-/>
+<img
+  src="/Home.png"
+  alt="Inpact AI platform showcase featuring creator-brand collaboration tools and analytics dashboard"
+  className="rounded-xl object-cover w-full h-auto shadow-lg"
+  onError={(e) => {
+    e.currentTarget.src = '/fallback-hero.png';
+  }}
+  loading="lazy"
+/>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img
src="/Home.png"
alt="Hero Image"
className="rounded-xl object-cover w-full h-auto shadow-lg"
/>
<img
src="/Home.png"
alt="Inpact AI platform showcase featuring creator-brand collaboration tools and analytics dashboard"
className="rounded-xl object-cover w-full h-auto shadow-lg"
onError={(e) => {
e.currentTarget.src = '/fallback-hero.png';
}}
loading="lazy"
/>
🤖 Prompt for AI Agents
In Frontend/src/pages/HomePage.tsx around lines 804 to 808, the img element uses
a generic alt text "Hero Image" and lacks error handling. Update the alt
attribute to a more descriptive text that conveys the image's purpose or content
for better accessibility. Additionally, implement error handling for the image
loading failure, such as adding an onError handler to provide fallback behavior
or display an alternative element.

<div className="space-y-6 w-full">
{/* Main Welcome Heading */}
<h1 className="text-5xl lg:text-7xl font-bold tracking-tight text-gray-900 leading-tight w-full text-center lg:text-left">
Welcome, <span className="bg-gradient-to-r from-purple-600 via-blue-600 to-indigo-600 bg-clip-text text-transparent animate-gradient">{user.user_metadata?.name || user.email?.split('@')[0]}</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve user data access with proper fallback handling.

The user name extraction could be more robust and handle edge cases better.

-Welcome, <span className="bg-gradient-to-r from-purple-600 via-blue-600 to-indigo-600 bg-clip-text text-transparent animate-gradient">{user.user_metadata?.name || user.email?.split('@')[0]}</span>
+Welcome, <span className="bg-gradient-to-r from-purple-600 via-blue-600 to-indigo-600 bg-clip-text text-transparent animate-gradient">
+  {user.user_metadata?.name || 
+   user.user_metadata?.full_name || 
+   user.email?.split('@')[0] || 
+   'Creator'}
+</span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Welcome, <span className="bg-gradient-to-r from-purple-600 via-blue-600 to-indigo-600 bg-clip-text text-transparent animate-gradient">{user.user_metadata?.name || user.email?.split('@')[0]}</span>
Welcome, <span className="bg-gradient-to-r from-purple-600 via-blue-600 to-indigo-600 bg-clip-text text-transparent animate-gradient">
{user.user_metadata?.name ||
user.user_metadata?.full_name ||
user.email?.split('@')[0] ||
'Creator'}
</span>
🤖 Prompt for AI Agents
In Frontend/src/pages/HomePage.tsx at line 514, improve the user name extraction
by adding more robust fallback handling. Instead of directly accessing
user.user_metadata?.name or splitting user.email, add checks to ensure these
values exist and are valid strings before using them. Provide a sensible default
fallback string if neither is available to avoid potential runtime errors or
displaying undefined values.

Comment on lines +488 to +491
src="/Home.png"
alt="Dashboard Preview"
className="rounded-xl object-cover w-full h-auto shadow-lg"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add proper alt text and error handling for images.

The image lacks descriptive alt text and has no error handling for failed loads.

-<img
-  src="/Home.png"
-  alt="Dashboard Preview"
-  className="rounded-xl object-cover w-full h-auto shadow-lg"
-/>
+<img
+  src="/Home.png"
+  alt="Inpact AI dashboard interface showing analytics, collaboration tools, and creator-brand matching features"
+  className="rounded-xl object-cover w-full h-auto shadow-lg"
+  onError={(e) => {
+    e.currentTarget.src = '/fallback-dashboard.png';
+  }}
+  loading="lazy"
+/>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
src="/Home.png"
alt="Dashboard Preview"
className="rounded-xl object-cover w-full h-auto shadow-lg"
/>
<img
src="/Home.png"
alt="Inpact AI dashboard interface showing analytics, collaboration tools, and creator-brand matching features"
className="rounded-xl object-cover w-full h-auto shadow-lg"
onError={(e) => {
e.currentTarget.src = '/fallback-dashboard.png';
}}
loading="lazy"
/>
🤖 Prompt for AI Agents
In Frontend/src/pages/HomePage.tsx around lines 488 to 491, the image element
uses a generic alt text "Dashboard Preview" which is not descriptive, and there
is no error handling for image load failures. Update the alt attribute to a more
descriptive text that clearly conveys the image content. Additionally, implement
error handling for the image by adding an onError event handler that sets a
fallback image or handles the error gracefully to improve accessibility and user
experience.

Copy link
Member

@chandansgowda chandansgowda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving now.. but we'll have to make the code cleaner going forward.


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."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. how will gemini know this week's trend? is this grounded on google trends data?
  2. why are we making api calls.. lets use sdks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants