From 13d56317014341481780ed2595843ee29f12ab31 Mon Sep 17 00:00:00 2001 From: Saahi30 Date: Sat, 8 Nov 2025 16:02:50 +0530 Subject: [PATCH 01/15] feat(api): add advanced campaign filters to backend --- backend/app/api/routes/campaigns.py | 345 ++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 backend/app/api/routes/campaigns.py diff --git a/backend/app/api/routes/campaigns.py b/backend/app/api/routes/campaigns.py new file mode 100644 index 0000000..e600789 --- /dev/null +++ b/backend/app/api/routes/campaigns.py @@ -0,0 +1,345 @@ +""" +Campaign management routes for brand users. +""" +from fastapi import APIRouter, HTTPException, Depends, Query +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime +from app.core.supabase_clients import supabase_anon +from uuid import UUID + +router = APIRouter() + + +class CampaignCreate(BaseModel): + """Schema for creating a new campaign.""" + title: str = Field(..., min_length=1, max_length=255) + slug: Optional[str] = None + short_description: Optional[str] = None + description: Optional[str] = None + status: str = Field(default="draft", pattern="^(draft|active|paused|completed|archived)$") + platforms: List[str] = Field(default_factory=list) + deliverables: Optional[List[dict]] = Field(default_factory=list) + target_audience: Optional[dict] = Field(default_factory=dict) + budget_min: Optional[float] = None + budget_max: Optional[float] = None + preferred_creator_niches: List[str] = Field(default_factory=list) + preferred_creator_followers_range: Optional[str] = None + starts_at: Optional[datetime] = None + ends_at: Optional[datetime] = None + is_featured: bool = False + + +class CampaignUpdate(BaseModel): + """Schema for updating an existing campaign.""" + title: Optional[str] = None + slug: Optional[str] = None + short_description: Optional[str] = None + description: Optional[str] = None + status: Optional[str] = None + platforms: Optional[List[str]] = None + deliverables: Optional[List[dict]] = None + target_audience: Optional[dict] = None + budget_min: Optional[float] = None + budget_max: Optional[float] = None + preferred_creator_niches: Optional[List[str]] = None + preferred_creator_followers_range: Optional[str] = None + starts_at: Optional[datetime] = None + ends_at: Optional[datetime] = None + is_featured: Optional[bool] = None + + +class CampaignResponse(BaseModel): + """Schema for campaign response.""" + id: str + brand_id: str + title: str + slug: Optional[str] + short_description: Optional[str] + description: Optional[str] + status: str + platforms: List[str] + deliverables: List[dict] + target_audience: dict + budget_min: Optional[float] + budget_max: Optional[float] + preferred_creator_niches: List[str] + preferred_creator_followers_range: Optional[str] + created_at: datetime + updated_at: datetime + published_at: Optional[datetime] + starts_at: Optional[datetime] + ends_at: Optional[datetime] + is_featured: bool + + +async def get_brand_id_from_user(user_id: str) -> str: + """Get brand ID from user ID.""" + supabase = supabase_anon + + try: + response = supabase.table("brands").select("id").eq("user_id", user_id).single().execute() + + if not response.data: + raise HTTPException(status_code=404, detail="Brand profile not found") + + return response.data["id"] + except Exception as e: + if "PGRST116" in str(e): # No rows returned + raise HTTPException(status_code=404, detail="Brand profile not found") + raise HTTPException(status_code=500, detail=f"Error fetching brand profile: {str(e)}") + + +@router.post("/campaigns", response_model=CampaignResponse, status_code=201) +async def create_campaign(campaign: CampaignCreate, user_id: str = Query(..., description="User ID from authentication")): + """ + Create a new campaign for a brand. + + - **user_id**: The authenticated user's ID (should be passed from auth middleware) + - **campaign**: Campaign details matching the database schema + """ + supabase = supabase_anon + + # Get brand ID from user ID + brand_id = await get_brand_id_from_user(user_id) + + # Generate slug if not provided + if not campaign.slug: + import re + slug = re.sub(r'[^a-z0-9]+', '-', campaign.title.lower()).strip('-') + campaign.slug = f"{slug}-{datetime.now().strftime('%Y%m%d')}" + + try: + # Prepare campaign data + campaign_data = { + "brand_id": brand_id, + "title": campaign.title, + "slug": campaign.slug, + "short_description": campaign.short_description, + "description": campaign.description, + "status": campaign.status, + "platforms": campaign.platforms, + "deliverables": campaign.deliverables, + "target_audience": campaign.target_audience, + "budget_min": campaign.budget_min, + "budget_max": campaign.budget_max, + "preferred_creator_niches": campaign.preferred_creator_niches, + "preferred_creator_followers_range": campaign.preferred_creator_followers_range, + "starts_at": campaign.starts_at.isoformat() if campaign.starts_at else None, + "ends_at": campaign.ends_at.isoformat() if campaign.ends_at else None, + "is_featured": campaign.is_featured, + } + + # If status is active, set published_at + if campaign.status == "active": + campaign_data["published_at"] = datetime.now().isoformat() + + # Insert campaign + response = supabase.table("campaigns").insert(campaign_data).execute() + + if not response.data: + raise HTTPException(status_code=500, detail="Failed to create campaign") + + return response.data[0] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating campaign: {str(e)}") + + + +@router.get("/campaigns", response_model=List[CampaignResponse]) +async def get_campaigns( + user_id: str = Query(..., description="User ID from authentication"), + status: Optional[str] = Query(None, description="Filter by status"), + search: Optional[str] = Query(None, description="Search by title or description"), + platform: Optional[str] = Query(None, description="Filter by platform"), + budget_min: Optional[float] = Query(None, description="Minimum budget"), + budget_max: Optional[float] = Query(None, description="Maximum budget"), + starts_after: Optional[datetime] = Query(None, description="Campaign starts after this date"), + ends_before: Optional[datetime] = Query(None, description="Campaign ends before this date"), + limit: int = Query(50, ge=1, le=100), + offset: int = Query(0, ge=0) +): + """ + Get all campaigns for a brand with optional filters. + + - **user_id**: The authenticated user's ID + - **status**: Optional filter by campaign status + - **search**: Optional search term for title or description + - **platform**: Optional filter by platform + - **budget_min**: Optional minimum budget + - **budget_max**: Optional maximum budget + - **starts_after**: Optional filter for campaigns starting after this date + - **ends_before**: Optional filter for campaigns ending before this date + - **limit**: Maximum number of results (default: 50, max: 100) + - **offset**: Number of results to skip for pagination + """ + supabase = supabase_anon + + # Get brand ID from user ID + brand_id = await get_brand_id_from_user(user_id) + + try: + # Build query + query = supabase.table("campaigns").select("*").eq("brand_id", brand_id) + + # Apply filters + if status: + query = query.eq("status", status) + + if search: + # Search in title and description + query = query.or_(f"title.ilike.%{search}%,description.ilike.%{search}%") + + if platform: + query = query.contains("platforms", [platform]) + + if budget_min is not None: + query = query.gte("budget_min", budget_min) + + if budget_max is not None: + query = query.lte("budget_max", budget_max) + + if starts_after: + query = query.gte("starts_at", starts_after.isoformat()) + + if ends_before: + query = query.lte("ends_at", ends_before.isoformat()) + + # Apply pagination and ordering + query = query.order("created_at", desc=True).range(offset, offset + limit - 1) + + response = query.execute() + + return response.data if response.data else [] + + except Exception as e: + import traceback + tb = traceback.format_exc() + raise HTTPException( + status_code=500, + detail=f"Error fetching campaigns: {str(e)} | Traceback: {tb}" + ) + + +@router.get("/campaigns/{campaign_id}", response_model=CampaignResponse) +async def get_campaign( + campaign_id: str, + user_id: str = Query(..., description="User ID from authentication") +): + """ + Get a single campaign by ID. + + - **campaign_id**: The campaign ID + - **user_id**: The authenticated user's ID + """ + supabase = supabase_anon + + # Get brand ID from user ID + brand_id = await get_brand_id_from_user(user_id) + + try: + # Fetch campaign and verify ownership + response = supabase.table("campaigns").select("*").eq("id", campaign_id).eq("brand_id", brand_id).single().execute() + + if not response.data: + raise HTTPException(status_code=404, detail="Campaign not found") + + return response.data + + except Exception as e: + if "PGRST116" in str(e): # No rows returned + raise HTTPException(status_code=404, detail="Campaign not found") + raise HTTPException(status_code=500, detail=f"Error fetching campaign: {str(e)}") + + +@router.put("/campaigns/{campaign_id}", response_model=CampaignResponse) +async def update_campaign( + campaign_id: str, + campaign: CampaignUpdate, + user_id: str = Query(..., description="User ID from authentication") +): + """ + Update an existing campaign. + + - **campaign_id**: The campaign ID + - **campaign**: Updated campaign details + - **user_id**: The authenticated user's ID + """ + supabase = supabase_anon + + # Get brand ID from user ID + brand_id = await get_brand_id_from_user(user_id) + + try: + # Verify campaign exists and belongs to this brand + existing = supabase.table("campaigns").select("id").eq("id", campaign_id).eq("brand_id", brand_id).single().execute() + + if not existing.data: + raise HTTPException(status_code=404, detail="Campaign not found") + + # Prepare update data (only include non-None fields) + update_data = {k: v for k, v in campaign.dict().items() if v is not None} + + if not update_data: + raise HTTPException(status_code=400, detail="No fields to update") + + # Update timestamp + update_data["updated_at"] = datetime.now().isoformat() + + # If status changes to active and published_at is not set, set it + if update_data.get("status") == "active": + current = supabase.table("campaigns").select("published_at").eq("id", campaign_id).single().execute() + if current.data and not current.data.get("published_at"): + update_data["published_at"] = datetime.now().isoformat() + + # Update campaign + response = supabase.table("campaigns").update(update_data).eq("id", campaign_id).execute() + + if not response.data: + raise HTTPException(status_code=500, detail="Failed to update campaign") + + return response.data[0] + + except HTTPException: + raise + except Exception as e: + if "PGRST116" in str(e): + raise HTTPException(status_code=404, detail="Campaign not found") + raise HTTPException(status_code=500, detail=f"Error updating campaign: {str(e)}") + + +@router.delete("/campaigns/{campaign_id}", status_code=204) +async def delete_campaign( + campaign_id: str, + user_id: str = Query(..., description="User ID from authentication") +): + """ + Delete a campaign. + + - **campaign_id**: The campaign ID + - **user_id**: The authenticated user's ID + """ + supabase = supabase_anon + + # Get brand ID from user ID + brand_id = await get_brand_id_from_user(user_id) + + try: + # Verify campaign exists and belongs to this brand + existing = supabase.table("campaigns").select("id").eq("id", campaign_id).eq("brand_id", brand_id).single().execute() + + if not existing.data: + raise HTTPException(status_code=404, detail="Campaign not found") + + # Delete campaign + supabase.table("campaigns").delete().eq("id", campaign_id).execute() + + return None + + except HTTPException: + raise + except Exception as e: + if "PGRST116" in str(e): + raise HTTPException(status_code=404, detail="Campaign not found") + raise HTTPException(status_code=500, detail=f"Error deleting campaign: {str(e)}") From 80a7006dcc2fdbec064accc788e4a0810ab22a1f Mon Sep 17 00:00:00 2001 From: Saahi30 Date: Sat, 8 Nov 2025 16:02:56 +0530 Subject: [PATCH 02/15] feat(lib): add campaign API utilities for filters --- frontend/lib/campaignApi.ts | 253 ++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 frontend/lib/campaignApi.ts diff --git a/frontend/lib/campaignApi.ts b/frontend/lib/campaignApi.ts new file mode 100644 index 0000000..ae9d3e4 --- /dev/null +++ b/frontend/lib/campaignApi.ts @@ -0,0 +1,253 @@ +/** + * Campaign API utilities + */ + +import { Campaign, CampaignFilters, CampaignFormData } from "@/types/campaign"; + +const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +/** + * Get user ID from Supabase session + */ +async function getUserId(): Promise { + const { supabase } = await import("@/lib/supabaseClient"); + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) throw new Error("Not authenticated"); + return user.id; +} + +/** + * Fetch all campaigns for the current brand + */ +export async function fetchCampaigns( + filters?: CampaignFilters +): Promise { + const userId = await getUserId(); + + const params = new URLSearchParams({ user_id: userId }); + + if (filters?.status) { + params.append("status", filters.status); + } + if (filters?.search) { + params.append("search", filters.search); + } + if (filters?.platform) { + params.append("platform", filters.platform); + } + if (filters?.budget_min !== undefined) { + params.append("budget_min", String(filters.budget_min)); + } + if (filters?.budget_max !== undefined) { + params.append("budget_max", String(filters.budget_max)); + } + if (filters?.starts_after) { + params.append("starts_after", filters.starts_after); + } + if (filters?.ends_before) { + params.append("ends_before", filters.ends_before); + } + + const response = await fetch(`${API_URL}/campaigns?${params.toString()}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || "Failed to fetch campaigns"); + } + + return response.json(); +} + +/** + * Fetch a single campaign by ID + */ +export async function fetchCampaignById(campaignId: string): Promise { + const userId = await getUserId(); + + const params = new URLSearchParams({ user_id: userId }); + + const response = await fetch( + `${API_URL}/campaigns/${campaignId}?${params.toString()}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || "Failed to fetch campaign"); + } + + return response.json(); +} + +/** + * Create a new campaign + */ +export async function createCampaign( + formData: CampaignFormData +): Promise { + const userId = await getUserId(); + + const params = new URLSearchParams({ user_id: userId }); + + // Transform form data to match API schema + const payload = { + title: formData.title, + short_description: formData.short_description || undefined, + description: formData.description || undefined, + status: formData.status, + platforms: formData.platforms, + deliverables: formData.deliverables, + target_audience: formData.target_audience, + budget_min: formData.budget_min + ? parseFloat(formData.budget_min) + : undefined, + budget_max: formData.budget_max + ? parseFloat(formData.budget_max) + : undefined, + preferred_creator_niches: formData.preferred_creator_niches, + preferred_creator_followers_range: + formData.preferred_creator_followers_range || undefined, + starts_at: formData.starts_at || undefined, + ends_at: formData.ends_at || undefined, + }; + + const response = await fetch(`${API_URL}/campaigns?${params.toString()}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || "Failed to create campaign"); + } + + return response.json(); +} + +/** + * Update an existing campaign + */ +export async function updateCampaign( + campaignId: string, + formData: Partial +): Promise { + const userId = await getUserId(); + + const params = new URLSearchParams({ user_id: userId }); + + // Transform form data to match API schema + const payload: any = {}; + + if (formData.title) payload.title = formData.title; + if (formData.short_description !== undefined) + payload.short_description = formData.short_description; + if (formData.description !== undefined) + payload.description = formData.description; + if (formData.status) payload.status = formData.status; + if (formData.platforms) payload.platforms = formData.platforms; + if (formData.deliverables) payload.deliverables = formData.deliverables; + if (formData.target_audience) + payload.target_audience = formData.target_audience; + if (formData.budget_min) payload.budget_min = parseFloat(formData.budget_min); + if (formData.budget_max) payload.budget_max = parseFloat(formData.budget_max); + if (formData.preferred_creator_niches) + payload.preferred_creator_niches = formData.preferred_creator_niches; + if (formData.preferred_creator_followers_range) + payload.preferred_creator_followers_range = + formData.preferred_creator_followers_range; + if (formData.starts_at) payload.starts_at = formData.starts_at; + if (formData.ends_at) payload.ends_at = formData.ends_at; + + const response = await fetch( + `${API_URL}/campaigns/${campaignId}?${params.toString()}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + } + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || "Failed to update campaign"); + } + + return response.json(); +} + +/** + * Delete a campaign + */ +export async function deleteCampaign(campaignId: string): Promise { + const userId = await getUserId(); + + const params = new URLSearchParams({ user_id: userId }); + + const response = await fetch( + `${API_URL}/campaigns/${campaignId}?${params.toString()}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || "Failed to delete campaign"); + } +} + +/** + * Format currency + */ +export function formatCurrency(amount: number): string { + return new Intl.NumberFormat("en-IN", { + style: "currency", + currency: "INR", + maximumFractionDigits: 0, + }).format(amount); +} + +/** + * Format date + */ +export function formatDate(dateString: string): string { + return new Date(dateString).toLocaleDateString("en-IN", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +/** + * Get status color class + */ +export function getStatusColor(status: string): string { + const colors: Record = { + draft: "bg-gray-100 text-gray-800", + active: "bg-green-100 text-green-800", + paused: "bg-yellow-100 text-yellow-800", + completed: "bg-blue-100 text-blue-800", + archived: "bg-slate-100 text-slate-800", + }; + return colors[status] || colors.draft; +} From 3b39dc18a65474565947b0d6e974576a1a6038eb Mon Sep 17 00:00:00 2001 From: Saahi30 Date: Sat, 8 Nov 2025 16:02:59 +0530 Subject: [PATCH 03/15] feat(types): update campaign types for new filters --- frontend/types/campaign.ts | 169 +++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 frontend/types/campaign.ts diff --git a/frontend/types/campaign.ts b/frontend/types/campaign.ts new file mode 100644 index 0000000..c1ee293 --- /dev/null +++ b/frontend/types/campaign.ts @@ -0,0 +1,169 @@ +/** + * Campaign Types - matching database schema + */ + +export interface Campaign { + id: string; + brand_id: string; + title: string; + slug?: string; + short_description?: string; + description?: string; + status: CampaignStatus; + platforms: string[]; + deliverables: CampaignDeliverable[]; + target_audience: TargetAudience; + budget_min?: number; + budget_max?: number; + preferred_creator_niches: string[]; + preferred_creator_followers_range?: string; + created_at: string; + updated_at: string; + published_at?: string; + starts_at?: string; + ends_at?: string; + is_featured: boolean; +} + +export type CampaignStatus = + | "draft" + | "active" + | "paused" + | "completed" + | "archived"; + +export interface CampaignDeliverable { + platform: string; + content_type: string; + quantity: number; + guidance?: string; + required: boolean; +} + +export interface TargetAudience { + age_groups?: string[]; + gender?: string[]; + locations?: string[]; + interests?: string[]; + income_level?: string[]; + description?: string; +} + +export interface CampaignFormData { + title: string; + short_description: string; + description: string; + status: CampaignStatus; + platforms: string[]; + deliverables: CampaignDeliverable[]; + target_audience: TargetAudience; + budget_min: string; + budget_max: string; + preferred_creator_niches: string[]; + preferred_creator_followers_range: string; + starts_at: string; + ends_at: string; +} + +export interface CampaignFilters { + status?: CampaignStatus; + search?: string; + platform?: string; + budget_min?: number; + budget_max?: number; + starts_after?: string; // ISO string + ends_before?: string; // ISO string +} + +// Platform options +export const PLATFORM_OPTIONS = [ + "Instagram", + "YouTube", + "TikTok", + "Twitter", + "LinkedIn", + "Facebook", + "Twitch", + "Blog", + "Podcast", +] as const; + +// Content type options +export const CONTENT_TYPE_OPTIONS = [ + "Post", + "Story", + "Reel", + "Video", + "Short", + "Live Stream", + "Article", + "Review", + "Tutorial", + "Unboxing", +] as const; + +// Niche options +export const NICHE_OPTIONS = [ + "Fashion", + "Beauty", + "Lifestyle", + "Travel", + "Food", + "Fitness", + "Gaming", + "Tech", + "Finance", + "Education", + "Entertainment", + "Health", + "Parenting", + "Business", + "Art", + "Music", + "Sports", +] as const; + +// Follower range options +export const FOLLOWER_RANGE_OPTIONS = [ + "1K-10K (Nano)", + "10K-50K (Micro)", + "50K-100K (Mid-tier)", + "100K-500K (Macro)", + "500K+ (Mega)", +] as const; + +// Status options with display labels +export const STATUS_OPTIONS: { + value: CampaignStatus; + label: string; + color: string; +}[] = [ + { value: "draft", label: "Draft", color: "gray" }, + { value: "active", label: "Active", color: "green" }, + { value: "paused", label: "Paused", color: "yellow" }, + { value: "completed", label: "Completed", color: "blue" }, + { value: "archived", label: "Archived", color: "slate" }, +]; + +// Age group options +export const AGE_GROUP_OPTIONS = [ + "13-17", + "18-24", + "25-34", + "35-44", + "45-54", + "55-64", + "65+", +] as const; + +// Gender options +export const GENDER_OPTIONS = ["Male", "Female", "Non-binary", "All"] as const; + +// Income level options +export const INCOME_LEVEL_OPTIONS = [ + "Low (<$25K)", + "Lower-middle ($25K-$50K)", + "Middle ($50K-$75K)", + "Upper-middle ($75K-$100K)", + "High ($100K+)", +] as const; From 23737dd58b3df86c1be73e9d077596a675290a4c Mon Sep 17 00:00:00 2001 From: Saahi30 Date: Sat, 8 Nov 2025 16:03:03 +0530 Subject: [PATCH 04/15] docs(guides): add campaign management documentation --- .../CAMPAIGN_MANAGEMENT_IMPLEMENTATION.md | 373 ++++++++++++++++++ guides/summaries/CAMPAIGN_QUICK_REFERENCE.md | 158 ++++++++ guides/summaries/CAMPAIGN_VISUAL_FLOW.md | 351 ++++++++++++++++ 3 files changed, 882 insertions(+) create mode 100644 guides/summaries/CAMPAIGN_MANAGEMENT_IMPLEMENTATION.md create mode 100644 guides/summaries/CAMPAIGN_QUICK_REFERENCE.md create mode 100644 guides/summaries/CAMPAIGN_VISUAL_FLOW.md diff --git a/guides/summaries/CAMPAIGN_MANAGEMENT_IMPLEMENTATION.md b/guides/summaries/CAMPAIGN_MANAGEMENT_IMPLEMENTATION.md new file mode 100644 index 0000000..a4fa6c9 --- /dev/null +++ b/guides/summaries/CAMPAIGN_MANAGEMENT_IMPLEMENTATION.md @@ -0,0 +1,373 @@ +# Campaign Management System Implementation Guide + +## Overview + +A comprehensive campaign management system for brand users to create, view, and manage influencer marketing campaigns. + +## Features Implemented + +### 1. **Main Campaign Hub** (`/brand/createcampaign`) + +- Modern landing page with two primary options: + - **View All Campaigns**: Navigate to campaign list + - **Create New Campaign**: Start a new campaign +- Quick stats dashboard (Total, Active, Applications) +- Beautiful gradient UI with hover effects + +### 2. **Campaign List View** (`/brand/campaigns`) + +- **Search & Filter**: + - Search campaigns by title or description + - Filter by status (Draft, Active, Paused, Completed, Archived) + - Real-time filtering + +- **Campaign Cards**: + - Summary view showing highlights (title, status, dates, budget, platforms) + - Expandable details on click + - Color-coded status badges + - Formatted currency and dates + +- **Detailed View** (Expandable): + - Full description + - Campaign duration + - Budget range + - Target platforms + - Preferred creator niches + - Follower range requirements + - Deliverables with quantity and guidance + - Target audience details + - Edit and view applications buttons + +### 3. **Create Campaign Form** (`/brand/campaigns/create`) + +Comprehensive form with all database schema fields: + +#### Basic Information + +- Campaign title (required) +- Short description +- Detailed description + +#### Campaign Duration + +- Start date +- End date + +#### Budget + +- Minimum budget (INR) +- Maximum budget (INR) + +#### Target Platforms + +- Multi-select from: Instagram, YouTube, TikTok, Twitter, LinkedIn, Facebook, Twitch, Blog, Podcast + +#### Deliverables + +- Dynamic deliverable builder +- Fields: Platform, Content Type, Quantity, Guidance, Required flag +- Add/remove multiple deliverables +- Visual list of added deliverables + +#### Creator Preferences + +- Preferred niches (multi-select from 17+ options) +- Follower range selection (Nano to Mega influencers) + +#### Target Audience + +- Age groups (multi-select) +- Gender (multi-select) +- Income levels (multi-select) +- Audience description (free text) + +#### Actions + +- Save as Draft +- Publish Campaign (sets status to active) +- Form validation +- Error handling + +## Technical Implementation + +### Backend (`/backend`) + +#### API Routes (`app/api/routes/campaigns.py`) + +- `POST /campaigns` - Create new campaign +- `GET /campaigns` - List all campaigns for a brand (with filters) +- `GET /campaigns/{id}` - Get single campaign details +- `PUT /campaigns/{id}` - Update campaign +- `DELETE /campaigns/{id}` - Delete campaign + +**Authentication**: User ID passed via query parameter (from auth middleware) + +**Features**: + +- Auto-slug generation from title +- Brand ID lookup from user ID +- Pagination support (limit/offset) +- Search functionality +- Status filtering +- Ownership verification +- Auto-publish timestamp on status change + +#### Database Schema Matching + +All fields match the `campaigns` table in PostgreSQL: + +```sql +CREATE TABLE public.campaigns ( + id uuid PRIMARY KEY, + brand_id uuid NOT NULL, + title text NOT NULL, + slug text UNIQUE, + short_description text, + description text, + status text NOT NULL DEFAULT 'draft', + platforms ARRAY, + deliverables jsonb, + target_audience jsonb, + budget_min numeric, + budget_max numeric, + preferred_creator_niches ARRAY, + preferred_creator_followers_range text, + created_at timestamp, + updated_at timestamp, + published_at timestamp, + starts_at timestamp, + ends_at timestamp, + is_featured boolean +) +``` + +### Frontend (`/frontend`) + +#### Types (`types/campaign.ts`) + +- `Campaign` - Full campaign interface +- `CampaignFormData` - Form data interface +- `CampaignDeliverable` - Deliverable structure +- `TargetAudience` - Audience targeting structure +- Constants for all dropdown options + +#### API Utilities (`lib/campaignApi.ts`) + +- `fetchCampaigns(filters)` - Get campaigns with filtering +- `fetchCampaignById(id)` - Get single campaign +- `createCampaign(data)` - Create new campaign +- `updateCampaign(id, data)` - Update campaign +- `deleteCampaign(id)` - Delete campaign +- Helper functions: `formatCurrency`, `formatDate`, `getStatusColor` + +#### Components + +1. **Main Hub** (`app/brand/createcampaign/page.tsx`) + - Two action cards with gradients + - Feature lists + - Stats placeholders + +2. **Campaign List** (`app/brand/campaigns/page.tsx`) + - Search bar with icon + - Status filter dropdown + - Empty state handling + - Loading state with spinner + - Error state with retry + - Expandable campaign cards + - Responsive grid layout + +3. **Create Form** (`app/brand/campaigns/create/page.tsx`) + - Multi-section form with clear headings + - Toggle buttons for multi-select fields + - Dynamic deliverable builder + - Form validation + - Loading states + - Error messages + +## Authentication & Authorization + +All routes are protected with `AuthGuard` component: + +- Requires "Brand" role +- Redirects to login if not authenticated +- Redirects to correct home if wrong role + +Backend verifies brand ownership for all operations. + +## UI/UX Features + +### Design System + +- **Colors**: Blue (primary), Purple (secondary), status-based colors +- **Gradients**: Subtle background gradients for visual appeal +- **Shadows**: Elevation with shadow-lg and shadow-md +- **Transitions**: Smooth hover and state transitions +- **Responsive**: Mobile-first design with breakpoints + +### User Experience + +- **Clear CTAs**: Large, obvious action buttons +- **Visual Feedback**: Loading spinners, hover effects, disabled states +- **Error Handling**: User-friendly error messages +- **Validation**: Client-side validation with helpful messages +- **Search**: Real-time search with Enter key support +- **Filters**: Easy-to-use dropdown filters +- **Expandable Details**: Minimize clutter, show on demand + +### Accessibility + +- Semantic HTML +- Clear labels +- Keyboard navigation support +- Color contrast compliance +- Focus states + +## Environment Variables + +### Backend (`.env`) + +```bash +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_KEY=your-supabase-anon-key +SUPABASE_SERVICE_KEY=your-service-role-key +DATABASE_URL=postgresql://... +``` + +### Frontend (`.env.local`) + +```bash +NEXT_PUBLIC_API_URL=http://localhost:8000 +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-key-here +``` + +## Running the Application + +### Backend + +```bash +cd backend +# Activate virtual environment +source venv/bin/activate # or venv\Scripts\activate on Windows +# Install dependencies +pip install -r requirements.txt +# Run server +uvicorn app.main:app --reload +``` + +### Frontend + +```bash +cd frontend +# Install dependencies +npm install +# Run development server +npm run dev +``` + +### Access Points + +- Frontend: http://localhost:3000 +- Backend API: http://localhost:8000 +- API Docs: http://localhost:8000/docs + +## Routes + +### Brand Routes + +- `/brand/createcampaign` - Main campaign hub +- `/brand/campaigns` - Campaign list view +- `/brand/campaigns/create` - Create new campaign +- `/brand/campaigns/edit/{id}` - Edit campaign (TODO) + +### API Endpoints + +- `GET /campaigns?user_id={id}&status={status}&search={term}` - List campaigns +- `POST /campaigns?user_id={id}` - Create campaign +- `GET /campaigns/{campaign_id}?user_id={id}` - Get campaign +- `PUT /campaigns/{campaign_id}?user_id={id}` - Update campaign +- `DELETE /campaigns/{campaign_id}?user_id={id}` - Delete campaign + +## Database Relationships + +``` +auth.users + ↓ +profiles (role: Brand) + ↓ +brands (brand profile data) + ↓ +campaigns (marketing campaigns) + ↓ +├── campaign_deliverables +├── campaign_applications +├── campaign_invites +└── deals +``` + +## Future Enhancements + +### Planned Features + +1. **Edit Campaign** - Form pre-populated with existing data +2. **Campaign Analytics** - Performance metrics and graphs +3. **Applications View** - See creator applications per campaign +4. **Bulk Actions** - Archive/delete multiple campaigns +5. **Export** - Download campaign data as CSV/PDF +6. **Duplicate Campaign** - Clone existing campaigns +7. **Campaign Templates** - Pre-filled templates for common campaign types +8. **Rich Text Editor** - Better description formatting +9. **Image Upload** - Campaign banners and media +10. **Collaboration** - Invite team members to manage campaigns + +### Technical Improvements + +1. **Pagination** - Implement proper pagination for large lists +2. **Caching** - Cache campaign data for better performance +3. **Optimistic Updates** - Update UI before API response +4. **Real-time Updates** - WebSocket for live updates +5. **Notifications** - Email/push notifications for applications +6. **Better Validation** - More comprehensive field validation +7. **Testing** - Unit and integration tests +8. **Error Logging** - Centralized error tracking + +## Troubleshooting + +### Common Issues + +**1. "Brand profile not found" error** + +- Ensure user has completed onboarding as a Brand +- Check that `brands` table has entry for user_id + +**2. Campaigns not loading** + +- Check backend is running on port 8000 +- Verify CORS settings allow frontend origin +- Check browser console for API errors + +**3. Form submission fails** + +- Check all required fields are filled +- Verify budget min < max +- Verify start date < end date +- Check backend logs for validation errors + +**4. Authentication errors** + +- Ensure user is logged in +- Verify Supabase credentials are correct +- Check auth token hasn't expired + +## Support + +For issues or questions: + +1. Check the console for error messages +2. Verify environment variables are set correctly +3. Ensure database schema matches expectations +4. Review API documentation at `/docs` + +## License + +This implementation is part of the InPactAI project. diff --git a/guides/summaries/CAMPAIGN_QUICK_REFERENCE.md b/guides/summaries/CAMPAIGN_QUICK_REFERENCE.md new file mode 100644 index 0000000..f3abc73 --- /dev/null +++ b/guides/summaries/CAMPAIGN_QUICK_REFERENCE.md @@ -0,0 +1,158 @@ +# Campaign Management System - Quick Reference + +## 🎯 What Was Built + +A complete campaign management system for brands with: + +- ✅ Main campaign hub with two options (View/Create) +- ✅ Campaign list page with search and filters +- ✅ Comprehensive campaign creation form +- ✅ Full REST API backend +- ✅ TypeScript types and utilities +- ✅ Modern, responsive UI + +## 📁 Files Created/Modified + +### Backend + +- `backend/app/api/routes/campaigns.py` - Campaign API endpoints (NEW) +- `backend/app/main.py` - Registered campaigns router (MODIFIED) + +### Frontend + +- `frontend/app/brand/createcampaign/page.tsx` - Main hub (MODIFIED) +- `frontend/app/brand/campaigns/page.tsx` - Campaign list (NEW) +- `frontend/app/brand/campaigns/create/page.tsx` - Create form (NEW) +- `frontend/types/campaign.ts` - TypeScript types (NEW) +- `frontend/lib/campaignApi.ts` - API utilities (NEW) + +### Documentation + +- `guides/summaries/CAMPAIGN_MANAGEMENT_IMPLEMENTATION.md` - Full guide (NEW) + +## 🚀 Quick Start + +1. **Start Backend**: + + ```bash + cd backend + uvicorn app.main:app --reload + ``` + +2. **Start Frontend**: + + ```bash + cd frontend + npm run dev + ``` + +3. **Navigate to**: http://localhost:3000/brand/createcampaign + +## 🔑 Key Routes + +- `/brand/createcampaign` - Campaign hub +- `/brand/campaigns` - View all campaigns +- `/brand/campaigns/create` - Create new campaign + +## 📊 API Endpoints + +``` +POST /campaigns - Create campaign +GET /campaigns - List campaigns (with filters) +GET /campaigns/{id} - Get campaign details +PUT /campaigns/{id} - Update campaign +DELETE /campaigns/{id} - Delete campaign +``` + +## ✨ Features + +### View Campaigns + +- Search by name/description +- Filter by status +- Expandable details +- Formatted dates and currency +- Status badges + +### Create Campaign + +- Basic info (title, description) +- Duration (start/end dates) +- Budget range (INR) +- Platform selection +- Deliverables builder +- Creator preferences +- Target audience +- Save as draft or publish + +## 🎨 UI Features + +- Modern gradient backgrounds +- Responsive design +- Loading states +- Error handling +- Empty states +- Hover effects +- Color-coded statuses +- Clean typography + +## 🔒 Security + +- AuthGuard protection on all routes +- Brand role verification +- Ownership checks in API +- User ID from Supabase auth + +## 📝 Database Schema Match + +All form fields map to the `campaigns` table: + +- ✅ title, description, status +- ✅ platforms (array) +- ✅ deliverables (jsonb) +- ✅ target_audience (jsonb) +- ✅ budget_min, budget_max +- ✅ dates (starts_at, ends_at) +- ✅ creator preferences + +## 🎯 Next Steps (Optional) + +1. Edit campaign functionality +2. Campaign analytics +3. View applications +4. Image uploads +5. Rich text editor +6. Email notifications + +## 📞 Testing + +1. Log in as a Brand user +2. Complete onboarding +3. Visit `/brand/createcampaign` +4. Click "Create New Campaign" +5. Fill out form and publish +6. View in campaign list +7. Search and filter + +## ⚠️ Important Notes + +- User must have Brand role and completed onboarding +- Backend must be running on port 8000 +- Supabase credentials must be configured +- User must have entry in `brands` table + +## 🐛 Troubleshooting + +**"Brand profile not found"** +→ Complete brand onboarding first + +**Campaigns not loading** +→ Check backend is running and CORS is configured + +**Form won't submit** +→ Check validation (title required, budget min < max) + +--- + +**Implementation Complete** ✅ +All features requested have been implemented with modern UI/UX and full database integration. diff --git a/guides/summaries/CAMPAIGN_VISUAL_FLOW.md b/guides/summaries/CAMPAIGN_VISUAL_FLOW.md new file mode 100644 index 0000000..7b940f3 --- /dev/null +++ b/guides/summaries/CAMPAIGN_VISUAL_FLOW.md @@ -0,0 +1,351 @@ +# Campaign Management System - Visual Flow + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ BRAND USER JOURNEY │ +└─────────────────────────────────────────────────────────────────────┘ + +1. Login → Complete Onboarding → Brand Home + ↓ + ┌─────────────┴─────────────┐ + │ /brand/createcampaign │ + │ (Campaign Hub) │ + └─────────┬─────────┬───────┘ + │ │ + ┌────────────┘ └────────────┐ + ↓ ↓ + ┌──────────────────────┐ ┌──────────────────────┐ + │ View All Campaigns │ │ Create New Campaign │ + │ /brand/campaigns │ │ /brand/campaigns/ │ + │ │ │ create │ + └──────────┬───────────┘ └──────────┬───────────┘ + │ │ + ↓ ↓ + ┌──────────────────────┐ ┌──────────────────────┐ + │ • Search Campaigns │ │ • Fill Form │ + │ • Filter by Status │ │ • Add Deliverables │ + │ • Expand Details │ │ • Set Budget │ + │ • Edit/View Apps │ │ • Save/Publish │ + └──────────────────────┘ └──────────────────────┘ +``` + +--- + +## 🏗️ Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ FRONTEND (Next.js) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Pages: Components: │ +│ • /brand/createcampaign • AuthGuard │ +│ • /brand/campaigns • SlidingMenu │ +│ • /brand/campaigns/create │ +│ │ +│ Utils: Types: │ +│ • lib/campaignApi.ts • types/campaign.ts │ +│ │ +└────────────────────────┬────────────────────────────────────────┘ + │ + │ HTTP/REST API + │ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ BACKEND (FastAPI) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Routes: Models: │ +│ • POST /campaigns • CampaignCreate │ +│ • GET /campaigns • CampaignUpdate │ +│ • GET /campaigns/{id} • CampaignResponse │ +│ • PUT /campaigns/{id} │ +│ • DELETE /campaigns/{id} │ +│ │ +└────────────────────────┬────────────────────────────────────────┘ + │ + │ Supabase Client + │ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ DATABASE (PostgreSQL/Supabase) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Tables: │ +│ • auth.users │ +│ • profiles (role: Brand) │ +│ • brands │ +│ • campaigns ← Main table │ +│ • campaign_deliverables │ +│ • campaign_applications │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 📋 Data Flow: Create Campaign + +``` +User fills form + ↓ +Clicks "Publish Campaign" + ↓ +Frontend validates form + ↓ +campaignApi.createCampaign(formData) + ↓ +Transform formData → API payload + ↓ +POST /campaigns?user_id={userId} + ↓ +Backend receives request + ↓ +Get brand_id from user_id + ↓ +Generate slug from title + ↓ +Insert into campaigns table + ↓ +Return created campaign + ↓ +Frontend shows success + ↓ +Navigate to /brand/campaigns + ↓ +Display new campaign in list +``` + +--- + +## 📋 Data Flow: View Campaigns + +``` +User visits /brand/campaigns + ↓ +Load campaigns on mount + ↓ +campaignApi.fetchCampaigns(filters) + ↓ +GET /campaigns?user_id={id}&status={status}&search={term} + ↓ +Backend receives request + ↓ +Get brand_id from user_id + ↓ +Build query with filters + ↓ +Fetch from campaigns table + ↓ +Return campaigns array + ↓ +Frontend displays list + ↓ +User can: + • Search + • Filter + • Expand details + • Edit campaign +``` + +--- + +## 🗂️ File Structure + +``` +InPactAI/ +├── backend/ +│ └── app/ +│ ├── main.py (✓ Modified - Added campaigns router) +│ └── api/ +│ └── routes/ +│ └── campaigns.py (✓ NEW - All endpoints) +│ +├── frontend/ +│ ├── app/ +│ │ └── brand/ +│ │ ├── createcampaign/ +│ │ │ └── page.tsx (✓ Modified - Hub page) +│ │ └── campaigns/ +│ │ ├── page.tsx (✓ NEW - List view) +│ │ └── create/ +│ │ └── page.tsx (✓ NEW - Form) +│ │ +│ ├── types/ +│ │ └── campaign.ts (✓ NEW - TypeScript types) +│ │ +│ └── lib/ +│ └── campaignApi.ts (✓ NEW - API utilities) +│ +└── guides/ + └── summaries/ + ├── CAMPAIGN_MANAGEMENT_IMPLEMENTATION.md (✓ NEW) + ├── CAMPAIGN_QUICK_REFERENCE.md (✓ NEW) + └── CAMPAIGN_VISUAL_FLOW.md (✓ NEW - This file) +``` + +--- + +## 🎨 UI Components Breakdown + +### Main Hub (`/brand/createcampaign`) + +``` +┌────────────────────────────────────────────────────┐ +│ Campaign Management │ +│ Create new campaigns or manage existing ones │ +├────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────┐ ┌───────────────────┐ │ +│ │ View All │ │ Create New │ │ +│ │ Campaigns │ │ Campaign │ │ +│ │ │ │ │ │ +│ │ [Blue Card] │ │ [Purple Card] │ │ +│ │ • Search & filter │ │ • Set goals │ │ +│ │ • View details │ │ • Define audience │ │ +│ │ • Track apps │ │ • Set budget │ │ +│ │ │ │ • Find creators │ │ +│ │ [Go to Campaigns] │ │ [Start Creating] │ │ +│ └───────────────────┘ └───────────────────┘ │ +│ │ +│ Quick Stats: │ +│ [Total: -] [Active: -] [Applications: -] │ +└────────────────────────────────────────────────────┘ +``` + +### Campaign List (`/brand/campaigns`) + +``` +┌────────────────────────────────────────────────────┐ +│ My Campaigns [+ Create Campaign] │ +├────────────────────────────────────────────────────┤ +│ [Search...] [Filter ▼] [Search] │ +├────────────────────────────────────────────────────┤ +│ │ +│ Campaign Title 1 [ACTIVE] │ +│ Brief description here... │ +│ 📅 Jan 1 | 💰 ₹50K-₹200K | 👥 Instagram, YouTube │ +│ [▼] │ +│ ───────────────────────────────────────────── │ +│ │ Full description... │ │ +│ │ Deliverables: 5 posts, 2 videos │ │ +│ │ [Edit] [View Applications] │ │ +│ ───────────────────────────────────────────── │ +│ │ +│ Campaign Title 2 [DRAFT] │ +│ Another campaign... │ +│ 📅 Feb 15 | 💰 ₹100K-₹500K | 👥 TikTok │ +│ [▼] │ +└────────────────────────────────────────────────────┘ +``` + +### Create Form (`/brand/campaigns/create`) + +``` +┌────────────────────────────────────────────────────┐ +│ ← Back Create New Campaign │ +├────────────────────────────────────────────────────┤ +│ 📝 Basic Information │ +│ Campaign Title * [________________] │ +│ Short Description [________________] │ +│ Detailed Description [____________] │ +│ [____________] │ +├────────────────────────────────────────────────────┤ +│ 📅 Campaign Duration │ +│ Start Date [____] End Date [____] │ +├────────────────────────────────────────────────────┤ +│ 💰 Budget Range (INR) │ +│ Min [_____] Max [_____] │ +├────────────────────────────────────────────────────┤ +│ 📱 Target Platforms │ +│ [Instagram] [YouTube] [TikTok] [Twitter]... │ +├────────────────────────────────────────────────────┤ +│ 📦 Campaign Deliverables │ +│ Platform [▼] Type [▼] Qty [1] [+ Add] │ +│ • Instagram - Post (Qty: 3) ☑ Required [🗑] │ +│ • YouTube - Video (Qty: 1) ☑ Required [🗑] │ +├────────────────────────────────────────────────────┤ +│ 👤 Creator Preferences │ +│ [Fashion] [Beauty] [Lifestyle]... │ +│ Follower Range [10K-50K (Micro) ▼] │ +├────────────────────────────────────────────────────┤ +│ 🎯 Target Audience │ +│ Age: [18-24] [25-34] [35-44]... │ +│ Gender: [Male] [Female] [All] │ +│ Income: [Middle] [Upper-middle]... │ +│ Description [________________] │ +├────────────────────────────────────────────────────┤ +│ [Cancel] [Save as Draft] [Publish Campaign] │ +└────────────────────────────────────────────────────┘ +``` + +--- + +## 🔄 State Management + +### Campaign List Page States + +- **Loading**: Shows spinner while fetching +- **Error**: Shows error message with retry +- **Empty**: Shows "No campaigns" with CTA +- **Loaded**: Shows campaign cards +- **Expanded**: Shows full details for selected campaign + +### Create Form States + +- **Initial**: Empty form +- **Filling**: User entering data +- **Validating**: Client-side validation +- **Submitting**: API call in progress +- **Success**: Navigate to list +- **Error**: Show error message, stay on form + +--- + +## 🎯 User Interactions + +### List Page + +1. **Search**: Type → Enter → API call → Update list +2. **Filter**: Select status → Auto API call → Update list +3. **Expand**: Click card → Toggle expanded state → Show details +4. **Edit**: Click edit → Navigate to edit page (TODO) +5. **Create**: Click button → Navigate to create page + +### Create Form + +1. **Select Platforms**: Click buttons → Toggle selection +2. **Add Deliverable**: Fill fields → Click Add → Append to list +3. **Remove Deliverable**: Click trash icon → Remove from list +4. **Select Niches**: Click buttons → Toggle selection +5. **Target Audience**: Click options → Toggle selection +6. **Save Draft**: Click → Validate → API → Navigate +7. **Publish**: Click → Validate → API → Navigate + +--- + +## ✅ Validation Rules + +### Required Fields + +- Campaign title + +### Conditional Validation + +- Budget: min < max (if both provided) +- Dates: start < end (if both provided) + +### Deliverable Validation + +- Platform and content type required to add + +### Data Types + +- Budget: Numeric +- Dates: ISO date format +- Arrays: Platform, niches, target audience +- JSONB: Deliverables, target audience + +--- + +This visual flow provides a complete overview of the campaign management system implementation! From 31ef1065d81de5b5a275f8c6d3c343eb9771159b Mon Sep 17 00:00:00 2001 From: Saahi30 Date: Sat, 8 Nov 2025 16:03:07 +0530 Subject: [PATCH 05/15] chore: update health route, SQL, and lockfile --- backend/SQL | 326 +++++++++++++++ backend/app/api/routes/health.py | 2 +- backend/app/main.py | 4 +- frontend/package-lock.json | 698 +------------------------------ 4 files changed, 345 insertions(+), 685 deletions(-) diff --git a/backend/SQL b/backend/SQL index d437e5c..c35b0c0 100644 --- a/backend/SQL +++ b/backend/SQL @@ -1,3 +1,4 @@ + -- Table for user profiles create table if not exists profiles ( id uuid references auth.users(id) on delete cascade, @@ -6,3 +7,328 @@ create table if not exists profiles ( created_at timestamp with time zone default timezone('utc', now()), primary key (id) ); +-- WARNING: This schema is for context only and is not meant to be run. +-- Table order and constraints may not be valid for execution. +-- i got this sceme from clicking the save as sql button in supabase + +CREATE TABLE public.brands ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + user_id uuid NOT NULL UNIQUE, + company_name text NOT NULL, + company_tagline text, + company_description text, + company_logo_url text, + company_cover_image_url text, + industry text NOT NULL, + sub_industry ARRAY, + company_size text, + founded_year integer, + headquarters_location text, + company_type text, + website_url text NOT NULL, + contact_email text, + contact_phone text, + social_media_links jsonb, + target_audience_age_groups ARRAY, + target_audience_gender ARRAY, + target_audience_locations ARRAY, + target_audience_interests ARRAY, + target_audience_income_level ARRAY, + target_audience_description text, + brand_values ARRAY, + brand_personality ARRAY, + brand_voice text, + brand_colors jsonb, + marketing_goals ARRAY, + campaign_types_interested ARRAY, + preferred_content_types ARRAY, + preferred_platforms ARRAY, + campaign_frequency text, + monthly_marketing_budget numeric, + influencer_budget_percentage double precision, + budget_per_campaign_min numeric, + budget_per_campaign_max numeric, + typical_deal_size numeric, + payment_terms text, + offers_product_only_deals boolean DEFAULT false, + offers_affiliate_programs boolean DEFAULT false, + affiliate_commission_rate double precision, + preferred_creator_niches ARRAY, + preferred_creator_size ARRAY, + preferred_creator_locations ARRAY, + minimum_followers_required integer, + minimum_engagement_rate double precision, + content_dos ARRAY, + content_donts ARRAY, + brand_safety_requirements ARRAY, + competitor_brands ARRAY, + exclusivity_required boolean DEFAULT false, + exclusivity_duration_months integer, + past_campaigns_count integer DEFAULT 0, + successful_partnerships ARRAY, + case_studies ARRAY, + average_campaign_roi double precision, + products_services ARRAY, + product_price_range text, + product_categories ARRAY, + seasonal_products boolean DEFAULT false, + product_catalog_url text, + business_verified boolean DEFAULT false, + payment_verified boolean DEFAULT false, + tax_id_verified boolean DEFAULT false, + profile_completion_percentage integer DEFAULT 0, + is_active boolean DEFAULT true, + is_featured boolean DEFAULT false, + is_verified_brand boolean DEFAULT false, + subscription_tier text DEFAULT 'free'::text, + featured_until timestamp with time zone, + ai_profile_summary text, + search_keywords ARRAY, + matching_score_base double precision DEFAULT 50.0, + total_deals_posted integer DEFAULT 0, + total_deals_completed integer DEFAULT 0, + total_spent numeric DEFAULT 0, + average_deal_rating double precision, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + last_active_at timestamp with time zone DEFAULT now(), + CONSTRAINT brands_pkey PRIMARY KEY (id), + CONSTRAINT brands_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) +); +CREATE TABLE public.campaign_applications ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + campaign_id uuid NOT NULL, + creator_id uuid NOT NULL, + profile_snapshot jsonb DEFAULT '{}'::jsonb, + message text, + proposed_amount numeric, + attachments jsonb DEFAULT '[]'::jsonb, + status USER-DEFINED DEFAULT 'applied'::application_status, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT campaign_applications_pkey PRIMARY KEY (id), + CONSTRAINT campaign_applications_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id), + CONSTRAINT campaign_applications_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id) +); +CREATE TABLE public.campaign_assets ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + campaign_id uuid, + deal_id uuid, + uploaded_by uuid, + url text NOT NULL, + type text, + meta jsonb DEFAULT '{}'::jsonb, + created_at timestamp with time zone DEFAULT now(), + CONSTRAINT campaign_assets_pkey PRIMARY KEY (id), + CONSTRAINT campaign_assets_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id), + CONSTRAINT campaign_assets_deal_id_fkey FOREIGN KEY (deal_id) REFERENCES public.deals(id), + CONSTRAINT campaign_assets_uploaded_by_fkey FOREIGN KEY (uploaded_by) REFERENCES public.profiles(id) +); +CREATE TABLE public.campaign_deliverables ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + campaign_id uuid NOT NULL, + platform text, + content_type text, + quantity integer DEFAULT 1, + guidance text, + required boolean DEFAULT true, + created_at timestamp with time zone DEFAULT now(), + CONSTRAINT campaign_deliverables_pkey PRIMARY KEY (id), + CONSTRAINT campaign_deliverables_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id) +); +CREATE TABLE public.campaign_invites ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + campaign_id uuid NOT NULL, + brand_id uuid NOT NULL, + creator_id uuid NOT NULL, + message text, + proposed_amount numeric, + status USER-DEFINED DEFAULT 'pending'::invite_status, + sent_at timestamp with time zone, + responded_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now(), + CONSTRAINT campaign_invites_pkey PRIMARY KEY (id), + CONSTRAINT campaign_invites_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id), + CONSTRAINT campaign_invites_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id), + CONSTRAINT campaign_invites_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id) +); +CREATE TABLE public.campaign_payments ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + deal_id uuid NOT NULL, + amount numeric NOT NULL, + currency text DEFAULT 'INR'::text, + method text, + status USER-DEFINED DEFAULT 'pending'::payment_status, + external_payment_ref text, + metadata jsonb DEFAULT '{}'::jsonb, + created_at timestamp with time zone DEFAULT now(), + paid_at timestamp with time zone, + CONSTRAINT campaign_payments_pkey PRIMARY KEY (id), + CONSTRAINT campaign_payments_deal_id_fkey FOREIGN KEY (deal_id) REFERENCES public.deals(id) +); +CREATE TABLE public.campaign_performance ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + campaign_id uuid NOT NULL, + deal_id uuid, + report_source text, + recorded_at timestamp with time zone NOT NULL DEFAULT now(), + impressions bigint, + clicks bigint, + views bigint, + watch_time bigint, + engagements bigint, + conversions bigint, + revenue numeric, + raw jsonb DEFAULT '{}'::jsonb, + CONSTRAINT campaign_performance_pkey PRIMARY KEY (id), + CONSTRAINT campaign_performance_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id), + CONSTRAINT campaign_performance_deal_id_fkey FOREIGN KEY (deal_id) REFERENCES public.deals(id) +); +CREATE TABLE public.campaigns ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + brand_id uuid NOT NULL, + title text NOT NULL, + slug text UNIQUE, + short_description text, + description text, + status text NOT NULL DEFAULT 'draft'::text, + platforms ARRAY DEFAULT ARRAY[]::text[], + deliverables jsonb DEFAULT '[]'::jsonb, + target_audience jsonb DEFAULT '{}'::jsonb, + budget_min numeric, + budget_max numeric, + preferred_creator_niches ARRAY DEFAULT ARRAY[]::text[], + preferred_creator_followers_range text, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + published_at timestamp with time zone, + starts_at timestamp with time zone, + ends_at timestamp with time zone, + is_featured boolean DEFAULT false, + CONSTRAINT campaigns_pkey PRIMARY KEY (id), + CONSTRAINT campaigns_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id) +); +CREATE TABLE public.creators ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + user_id uuid NOT NULL UNIQUE, + display_name text NOT NULL, + bio text, + tagline text, + profile_picture_url text, + cover_image_url text, + website_url text, + youtube_url text, + youtube_handle text, + youtube_subscribers integer, + instagram_url text, + instagram_handle text, + instagram_followers integer, + tiktok_url text, + tiktok_handle text, + tiktok_followers integer, + twitter_url text, + twitter_handle text, + twitter_followers integer, + twitch_url text, + twitch_handle text, + twitch_followers integer, + linkedin_url text, + facebook_url text, + primary_niche text NOT NULL, + secondary_niches ARRAY, + content_types ARRAY, + content_language ARRAY, + total_followers integer DEFAULT 0, + total_reach integer, + average_views integer, + engagement_rate double precision, + audience_age_primary text, + audience_age_secondary ARRAY, + audience_gender_split jsonb, + audience_locations jsonb, + audience_interests ARRAY, + average_engagement_per_post integer, + posting_frequency text, + best_performing_content_type text, + peak_posting_times jsonb, + years_of_experience integer, + content_creation_full_time boolean DEFAULT false, + team_size integer DEFAULT 1, + equipment_quality text, + editing_software ARRAY, + collaboration_types ARRAY, + preferred_brands_style ARRAY, + not_interested_in ARRAY, + rate_per_post numeric, + rate_per_video numeric, + rate_per_story numeric, + rate_per_reel numeric, + rate_negotiable boolean DEFAULT true, + accepts_product_only_deals boolean DEFAULT false, + minimum_deal_value numeric, + preferred_payment_terms text, + portfolio_links ARRAY, + past_brand_collaborations ARRAY, + case_study_links ARRAY, + media_kit_url text, + email_verified boolean DEFAULT false, + phone_verified boolean DEFAULT false, + identity_verified boolean DEFAULT false, + profile_completion_percentage integer DEFAULT 0, + is_active boolean DEFAULT true, + is_featured boolean DEFAULT false, + is_verified_creator boolean DEFAULT false, + featured_until timestamp with time zone, + ai_profile_summary text, + search_keywords ARRAY, + matching_score_base double precision DEFAULT 50.0, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + last_active_at timestamp with time zone DEFAULT now(), + social_platforms jsonb, + CONSTRAINT creators_pkey PRIMARY KEY (id), + CONSTRAINT creators_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.profiles(id) +); +CREATE TABLE public.deals ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + campaign_id uuid, + application_id uuid, + brand_id uuid NOT NULL, + creator_id uuid NOT NULL, + agreed_amount numeric, + payment_schedule jsonb DEFAULT '[]'::jsonb, + terms jsonb DEFAULT '{}'::jsonb, + status USER-DEFINED DEFAULT 'draft'::deal_status, + starts_at timestamp with time zone, + ends_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now(), + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT deals_pkey PRIMARY KEY (id), + CONSTRAINT deals_campaign_id_fkey FOREIGN KEY (campaign_id) REFERENCES public.campaigns(id), + CONSTRAINT deals_application_id_fkey FOREIGN KEY (application_id) REFERENCES public.campaign_applications(id), + CONSTRAINT deals_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id), + CONSTRAINT deals_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id) +); +CREATE TABLE public.match_scores ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + creator_id uuid NOT NULL, + brand_id uuid NOT NULL, + score double precision NOT NULL, + reasons jsonb DEFAULT '[]'::jsonb, + computed_at timestamp with time zone DEFAULT now(), + CONSTRAINT match_scores_pkey PRIMARY KEY (id), + CONSTRAINT match_scores_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES public.creators(id), + CONSTRAINT match_scores_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES public.brands(id) +); +CREATE TABLE public.profiles ( + id uuid NOT NULL, + name text NOT NULL, + role text NOT NULL CHECK (role = ANY (ARRAY['Creator'::text, 'Brand'::text])), + created_at timestamp with time zone DEFAULT timezone('utc'::text, now()), + onboarding_completed boolean DEFAULT false, + CONSTRAINT profiles_pkey PRIMARY KEY (id), + CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users(id) +); + + + diff --git a/backend/app/api/routes/health.py b/backend/app/api/routes/health.py index 6f42dbf..560eb49 100644 --- a/backend/app/api/routes/health.py +++ b/backend/app/api/routes/health.py @@ -17,7 +17,7 @@ def check_supabase(): This endpoint attempts to query Supabase to verify the connection. """ try: - from app.services.supabase_client import supabase + from app.core.supabase_clients import supabase_anon as supabase # Attempt a simple query to verify connection response = supabase.table("_supabase_test").select("*").limit(1).execute() diff --git a/backend/app/main.py b/backend/app/main.py index 47a14f2..c79345a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,9 +2,10 @@ from fastapi.middleware.cors import CORSMiddleware import os from app.api.routes import health -from app.services.supabase_client import supabase +from app.core.supabase_clients import supabase_anon as supabase from app.api.routes import auth from app.api.routes import gemini_generate +from app.api.routes import campaigns app = FastAPI(title="Inpact Backend", version="0.1.0") # Verify Supabase client initialization on startup @@ -30,6 +31,7 @@ app.include_router(gemini_generate.router) app.include_router(health.router) app.include_router(auth.router) +app.include_router(campaigns.router) @app.get("/") def root(): diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c73e66d..6bc0efe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,6 @@ "name": "frontend", "version": "0.1.0", "dependencies": { - "@google/genai": "^1.29.0", "@hookform/resolvers": "^5.2.2", "@supabase/supabase-js": "^2.78.0", "@tanstack/react-query": "^5.90.5", @@ -489,27 +488,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@google/genai": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.29.0.tgz", - "integrity": "sha512-cQP7Ssa06W+MSAyVtL/812FBtZDoDehnFObIpK1xo5Uv4XvqBcVZ8OhXgihOIXWn7xvPQGvLclR8+yt3Ysnd9g==", - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.20.1" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, "node_modules/@hookform/resolvers": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", @@ -1002,23 +980,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1274,16 +1235,6 @@ "node": ">=12.4.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1733,7 +1684,7 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -2351,15 +2302,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2377,22 +2319,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2655,26 +2586,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "dev": true, "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -2687,15 +2599,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2754,12 +2657,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2866,6 +2763,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2878,6 +2776,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -2910,6 +2809,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2924,7 +2824,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2934,15 +2834,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3001,6 +2892,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3103,21 +2995,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.244", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", @@ -3129,6 +3006,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/enhanced-resolve": { @@ -3765,12 +3643,6 @@ "node": ">=0.10.0" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3832,29 +3704,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3955,22 +3804,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -3987,18 +3820,6 @@ "node": ">= 6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/framer-motion": { "version": "12.23.24", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", @@ -4066,35 +3887,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -4183,26 +3975,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4216,30 +3988,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4270,33 +4018,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.2.tgz", - "integrity": "sha512-YsFPGVgDFf4IzSwbwIR0iaFJQFmR5Jp7V1WuYSjuRgAm9yWqsMhKE9YPlL+wvFLnc/wMiFV4SQUD9Y/JMpxIxQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4323,19 +4044,6 @@ "dev": true, "license": "MIT" }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", - "license": "MIT", - "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4444,19 +4152,6 @@ "hermes-estree": "0.25.1" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4693,15 +4388,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4944,6 +4630,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -4964,21 +4651,6 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -5022,15 +4694,6 @@ "node": ">=6" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5081,27 +4744,6 @@ "node": ">=4.0" } }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5549,15 +5191,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/motion-dom": { "version": "12.23.23", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", @@ -5577,6 +5210,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5700,44 +5334,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -5936,12 +5532,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5969,6 +5559,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5981,28 +5572,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6260,21 +5829,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6319,26 +5873,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -6499,6 +6033,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6511,6 +6046,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6592,18 +6128,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6634,65 +6158,6 @@ "node": ">= 0.4" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6806,43 +6271,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -7278,15 +6706,6 @@ "punycode": "^2.1.0" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -7307,6 +6726,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7417,94 +6837,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", From 98e7e3c10e1095b3ec917ec7480ddd48d97b3ac3 Mon Sep 17 00:00:00 2001 From: Saahi30 Date: Sat, 8 Nov 2025 16:03:51 +0530 Subject: [PATCH 06/15] feat(frontend): add advanced campaign filters and UI for viewing campaigns --- frontend/app/brand/campaigns/create/page.tsx | 667 +++++++++++++++++++ frontend/app/brand/campaigns/page.tsx | 489 ++++++++++++++ frontend/app/brand/createcampaign/page.tsx | 146 +++- 3 files changed, 1296 insertions(+), 6 deletions(-) create mode 100644 frontend/app/brand/campaigns/create/page.tsx create mode 100644 frontend/app/brand/campaigns/page.tsx diff --git a/frontend/app/brand/campaigns/create/page.tsx b/frontend/app/brand/campaigns/create/page.tsx new file mode 100644 index 0000000..e8f6f48 --- /dev/null +++ b/frontend/app/brand/campaigns/create/page.tsx @@ -0,0 +1,667 @@ +"use client"; + +import AuthGuard from "@/components/auth/AuthGuard"; +import SlidingMenu from "@/components/SlidingMenu"; +import { createCampaign } from "@/lib/campaignApi"; +import { + AGE_GROUP_OPTIONS, + CampaignDeliverable, + CampaignFormData, + CONTENT_TYPE_OPTIONS, + FOLLOWER_RANGE_OPTIONS, + GENDER_OPTIONS, + INCOME_LEVEL_OPTIONS, + NICHE_OPTIONS, + PLATFORM_OPTIONS, +} from "@/types/campaign"; +import { ArrowLeft, Eye, Plus, Save, Trash2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +export default function CreateCampaignPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [formData, setFormData] = useState({ + title: "", + short_description: "", + description: "", + status: "draft", + platforms: [], + deliverables: [], + target_audience: {}, + budget_min: "", + budget_max: "", + preferred_creator_niches: [], + preferred_creator_followers_range: "", + starts_at: "", + ends_at: "", + }); + + const [newDeliverable, setNewDeliverable] = useState({ + platform: "", + content_type: "", + quantity: 1, + guidance: "", + required: true, + }); + + const updateField = (field: keyof CampaignFormData, value: any) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const toggleArrayField = ( + field: "platforms" | "preferred_creator_niches", + value: string + ) => { + setFormData((prev) => ({ + ...prev, + [field]: prev[field].includes(value) + ? prev[field].filter((item) => item !== value) + : [...prev[field], value], + })); + }; + + const updateTargetAudience = (field: string, value: any) => { + setFormData((prev) => ({ + ...prev, + target_audience: { + ...prev.target_audience, + [field]: value, + }, + })); + }; + + const toggleTargetAudienceArray = (field: string, value: string) => { + const current = + (formData.target_audience[ + field as keyof typeof formData.target_audience + ] as string[]) || []; + updateTargetAudience( + field, + current.includes(value) + ? current.filter((item) => item !== value) + : [...current, value] + ); + }; + + const addDeliverable = () => { + if (!newDeliverable.platform || !newDeliverable.content_type) { + alert("Please select platform and content type"); + return; + } + setFormData((prev) => ({ + ...prev, + deliverables: [...prev.deliverables, { ...newDeliverable }], + })); + setNewDeliverable({ + platform: "", + content_type: "", + quantity: 1, + guidance: "", + required: true, + }); + }; + + const removeDeliverable = (index: number) => { + setFormData((prev) => ({ + ...prev, + deliverables: prev.deliverables.filter((_, i) => i !== index), + })); + }; + + const validateForm = (): boolean => { + if (!formData.title.trim()) { + setError("Campaign title is required"); + return false; + } + if (formData.budget_min && formData.budget_max) { + if (parseFloat(formData.budget_min) > parseFloat(formData.budget_max)) { + setError("Minimum budget cannot be greater than maximum budget"); + return false; + } + } + if (formData.starts_at && formData.ends_at) { + if (new Date(formData.starts_at) > new Date(formData.ends_at)) { + setError("Start date cannot be after end date"); + return false; + } + } + return true; + }; + + const handleSubmit = async (status: "draft" | "active") => { + setError(null); + + if (!validateForm()) { + return; + } + + try { + setLoading(true); + const submitData = { ...formData, status }; + await createCampaign(submitData); + router.push("/brand/campaigns"); + } catch (err: any) { + setError(err.message || "Failed to create campaign"); + } finally { + setLoading(false); + } + }; + + return ( + +
+ + +
+ {/* Header */} +
+ +

+ Create New Campaign +

+

+ Fill out the details below to launch your influencer marketing + campaign +

+
+ + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + +
+ {/* Basic Information */} +
+

+ Basic Information +

+
+
+ + updateField("title", e.target.value)} + placeholder="e.g., Summer Product Launch 2024" + className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none" + required + /> +
+ +
+ + + updateField("short_description", e.target.value) + } + placeholder="Brief one-liner about your campaign" + className="w-full rounded-lg border border-gray-300 px-4 py-3 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:outline-none" + /> +
+ +
+ +