Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions frontend/lib/api/collaborations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { authenticatedFetch } from "../auth-helpers";

const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";

export interface CollaborationIdea {
title: string;
description: string;
collaboration_type: string;
why_it_works: string;
}

export interface CollaborationIdeasResponse {
ideas: CollaborationIdea[];
}

/**
* Generate collaboration ideas between the current creator and a target creator
*/
export async function generateCollaborationIdeas(
targetCreatorId: string
): Promise<CollaborationIdeasResponse> {
const url = `${API_BASE_URL}/collaborations/generate-ideas`;
const response = await authenticatedFetch(url, {
method: "POST",
body: JSON.stringify({ target_creator_id: targetCreatorId }),
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.detail || `Failed to generate collaboration ideas: ${response.statusText}`
);
}

return response.json();
}

export interface CreatorRecommendationForIdea {
creator_id: string;
display_name: string;
profile_picture_url: string | null;
primary_niche: string;
match_score: number;
reasoning: string;
}

export interface RecommendCreatorResponse {
recommended_creator: CreatorRecommendationForIdea;
alternatives: CreatorRecommendationForIdea[];
}

/**
* Recommend the best creator from a list of candidates for a collaboration idea
*/
export async function recommendCreatorForIdea(
collaborationIdea: string,
candidateCreatorIds: string[]
): Promise<RecommendCreatorResponse> {
const url = `${API_BASE_URL}/collaborations/recommend-creator`;
const response = await authenticatedFetch(url, {
method: "POST",
body: JSON.stringify({
collaboration_idea: collaborationIdea,
candidate_creator_ids: candidateCreatorIds,
}),
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.detail || `Failed to get recommendation: ${response.statusText}`
);
}

return response.json();
}

167 changes: 167 additions & 0 deletions frontend/lib/api/creators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { authenticatedFetch } from "../auth-helpers";

const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";

export interface CreatorBasic {
id: string;
display_name: string;
tagline: string | null;
bio: string | null;
profile_picture_url: string | null;
primary_niche: string;
secondary_niches: string[] | null;
total_followers: number;
engagement_rate: number | null;
is_verified_creator: boolean;
profile_completion_percentage: number;
}

export interface CreatorFull extends CreatorBasic {
user_id: string;
cover_image_url: string | null;
website_url: string | null;
youtube_url: string | null;
youtube_handle: string | null;
youtube_subscribers: number | null;
instagram_url: string | null;
instagram_handle: string | null;
instagram_followers: number | null;
tiktok_url: string | null;
tiktok_handle: string | null;
tiktok_followers: number | null;
twitter_url: string | null;
twitter_handle: string | null;
twitter_followers: number | null;
twitch_url: string | null;
twitch_handle: string | null;
twitch_followers: number | null;
linkedin_url: string | null;
facebook_url: string | null;
content_types: string[] | null;
content_language: string[] | null;
total_reach: number | null;
average_views: number | null;
audience_age_primary: string | null;
audience_gender_split: Record<string, any> | null;
audience_locations: Record<string, any> | null;
audience_interests: string[] | null;
average_engagement_per_post: number | null;
posting_frequency: string | null;
best_performing_content_type: string | null;
years_of_experience: number | null;
content_creation_full_time: boolean;
team_size: number;
equipment_quality: string | null;
editing_software: string[] | null;
collaboration_types: string[] | null;
preferred_brands_style: string[] | null;
rate_per_post: number | null;
rate_per_video: number | null;
rate_per_story: number | null;
rate_per_reel: number | null;
rate_negotiable: boolean;
accepts_product_only_deals: boolean;
minimum_deal_value: number | null;
preferred_payment_terms: string | null;
portfolio_links: string[] | null;
past_brand_collaborations: string[] | null;
case_study_links: string[] | null;
media_kit_url: string | null;
created_at: string | null;
last_active_at: string | null;
}

export interface ListCreatorsParams {
search?: string;
niche?: string;
limit?: number;
offset?: number;
}

/**
* List all creators with optional search and filters
*/
export async function listCreators(
params: ListCreatorsParams = {}
): Promise<CreatorBasic[]> {
const queryParams = new URLSearchParams();
if (params.search) queryParams.append("search", params.search);
if (params.niche) queryParams.append("niche", params.niche);
if (params.limit) queryParams.append("limit", params.limit.toString());
if (params.offset) queryParams.append("offset", params.offset.toString());

const url = `${API_BASE_URL}/creators?${queryParams.toString()}`;
const response = await authenticatedFetch(url);

if (!response.ok) {
// Try to get error details from response
let errorMessage = `Failed to fetch creators: ${response.statusText}`;
try {
const errorData = await response.json();
if (errorData.detail) {
errorMessage = errorData.detail;
}
} catch {
// If response is not JSON, use status text
}

const error = new Error(errorMessage);
(error as any).status = response.status;
throw error;
}

return response.json();
}

/**
* Get full details of a specific creator
*/
export async function getCreatorDetails(creatorId: string): Promise<CreatorFull> {
const url = `${API_BASE_URL}/creators/${creatorId}`;
const response = await authenticatedFetch(url);

if (!response.ok) {
throw new Error(`Failed to fetch creator details: ${response.statusText}`);
}

return response.json();
}

export interface CreatorRecommendation {
id: string;
display_name: string;
profile_picture_url: string | null;
primary_niche: string | null;
total_followers: number | null;
engagement_rate: number | null;
top_platforms?: string[] | null;
match_score: number;
reason: string;
}

export async function getCreatorRecommendations(limit = 4): Promise<CreatorRecommendation[]> {
const url = `${API_BASE_URL}/creators/recommendations?limit=${limit}`;
const response = await authenticatedFetch(url);

if (!response.ok) {
const errText = await response.text().catch(() => "");
throw new Error(`Failed to fetch recommendations: ${response.status} ${errText}`);
}
return response.json();
}

/**
* Get list of all unique niches
*/
export async function listNiches(): Promise<string[]> {
const url = `${API_BASE_URL}/creators/niches/list`;
const response = await authenticatedFetch(url);

if (!response.ok) {
throw new Error(`Failed to fetch niches: ${response.statusText}`);
}

const data = await response.json();
return data.niches || [];
}

106 changes: 93 additions & 13 deletions frontend/lib/auth-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { supabase } from "./supabaseClient";
import { getAuthToken, supabase } from "./supabaseClient";

export interface UserProfile {
id: string;
Expand All @@ -16,7 +16,9 @@ export async function getCurrentUser() {
data: { user },
error,
} = await supabase.auth.getUser();
if (error) throw error;
if (error || !user) {
return null;
}
return user;
}

Expand All @@ -34,7 +36,10 @@ export async function getUserProfile(): Promise<UserProfile | null> {
.eq("id", user.id)
.single();

if (error) throw error;
if (error) {
console.error("Error fetching profile:", error);
return null;
}
return data as UserProfile;
} catch (error) {
console.error("Error fetching user profile:", error);
Expand All @@ -53,16 +58,9 @@ export async function signOut() {
/**
* Check if user has a specific role
*/
export async function checkUserRole(
requiredRole: "Creator" | "Brand"
): Promise<boolean> {
try {
const profile = await getUserProfile();
return profile?.role === requiredRole;
} catch (error) {
console.error("Error checking user role:", error);
return false;
}
export async function checkUserRole(): Promise<"Creator" | "Brand" | null> {
const profile = await getUserProfile();
return profile?.role || null;
}

/**
Expand All @@ -78,6 +76,88 @@ export async function hasCompletedOnboarding(): Promise<boolean> {
}
}

/**
* Token refresh handling (automatic with Supabase)
*/
export async function ensureValidToken() {
const { data: { session } } = await supabase.auth.getSession();

if (!session) {
throw new Error("No active session");
}

// Supabase automatically refreshes if needed
return session.access_token;
}

/**
* Make authenticated API calls to backend
*/
export async function authenticatedFetch(url: string, options: RequestInit = {}) {
// Try to get token, refresh if needed
let token = await getAuthToken();

// If no token, try to refresh session
if (!token) {
try {
const { data: { session }, error } = await supabase.auth.refreshSession();
if (error) {
console.error("Failed to refresh session:", error);
// Don't throw here - let the 401 response handle it
token = undefined;
} else {
token = session?.access_token || undefined;
}
} catch (err) {
console.error("Error refreshing token:", err);
token = undefined;
}
}

if (!token) {
// Return a response that will trigger 401, don't throw
// This allows the calling code to handle it gracefully
return new Response(
JSON.stringify({ error: "No authentication token available" }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}

const headers = {
...options.headers,
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
};

const response = await fetch(url, {
...options,
headers,
});

// If 401, try refreshing token once
if (response.status === 401) {
try {
const { data: { session }, error } = await supabase.auth.refreshSession();
if (!error && session?.access_token) {
// Retry with new token
const retryHeaders = {
...options.headers,
"Authorization": `Bearer ${session.access_token}`,
"Content-Type": "application/json",
};
return fetch(url, {
...options,
headers: retryHeaders,
});
}
} catch (err) {
console.error("Error refreshing token on 401:", err);
}
}

return response;
}

/**
* Map Supabase auth errors to user-friendly messages
*/
Expand Down
Loading