-
Notifications
You must be signed in to change notification settings - Fork 140
FEAT(profile): add profile pages, components, and API integration #180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThis PR introduces comprehensive profile management for brands and creators with backend API endpoints for CRUD operations, profile completion tracking, and AI-assisted profile population via Gemini. The frontend provides complete profile pages with editing capabilities, form management, and UI components for rendering profile data. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Frontend as Frontend App
participant API as Backend API
participant DB as Supabase DB
participant Gemini as Gemini API
User->>Frontend: View Profile Page
Frontend->>API: GET /brand/profile
API->>DB: Fetch brand record
DB-->>API: Return profile + completion %
API-->>Frontend: Profile data
Frontend->>Frontend: Display profile & form
User->>Frontend: Enter edit mode & modify fields
Frontend->>Frontend: Update local formData state
User->>Frontend: Click Save
Frontend->>API: PUT /brand/profile
API->>API: Sanitize restricted fields
API->>API: Calculate completion %
API->>DB: Update brand record
DB-->>API: Confirm update
API-->>Frontend: Updated profile
Frontend->>Frontend: Exit edit mode, show success
rect rgb(200, 220, 240)
note over User,Gemini: AI-Fill Flow
User->>Frontend: Click AI Fill
Frontend->>Frontend: Open modal, accept input
User->>Frontend: Enter context + submit
Frontend->>API: POST /brand/profile/ai-fill
API->>Gemini: Send user input + context
Gemini-->>API: Return JSON with filled fields
API->>API: Parse & merge with existing profile
API-->>Frontend: Merged profile data
Frontend->>Frontend: Update formData, enable edit, show alert
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas requiring extra attention:
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🧹 Nitpick comments (1)
backend/app/api/routes/profiles.py (1)
42-44: Remove unused locals to satisfy Ruff F841.Both completion helpers assign
total = ...(Line 44 / Line 86) but never use it. Ruff flags this as F841, which fails CI. Please drop those assignments.- completed = 0 - total = len(required_fields) + len(important_fields) + len(nice_to_have_fields) + completed = 0Repeat in
calculate_creator_completion_percentage.Also applies to: 85-87
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
backend/app/api/routes/profiles.py(1 hunks)backend/app/main.py(2 hunks)frontend/app/brand/home/page.tsx(2 hunks)frontend/app/brand/profile/page.tsx(1 hunks)frontend/app/creator/home/page.tsx(2 hunks)frontend/app/creator/profile/page.tsx(1 hunks)frontend/components/profile/ArrayInput.tsx(1 hunks)frontend/components/profile/CollapsibleSection.tsx(1 hunks)frontend/components/profile/JsonInput.tsx(1 hunks)frontend/components/profile/ProfileButton.tsx(1 hunks)frontend/lib/api/profile.ts(1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.4)
backend/app/api/routes/profiles.py
43-43: Local variable total is assigned to but never used
Remove assignment to unused variable total
(F841)
85-85: Local variable total is assigned to but never used
Remove assignment to unused variable total
(F841)
110-110: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
125-125: Consider moving this statement to an else block
(TRY300)
129-129: Use explicit conversion flag
Replace with conversion flag
(RUF010)
136-136: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
158-161: Abstract raise to an inner function
(TRY301)
176-176: Consider moving this statement to an else block
(TRY300)
182-182: Use explicit conversion flag
Replace with conversion flag
(RUF010)
188-188: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
203-203: Consider moving this statement to an else block
(TRY300)
207-207: Use explicit conversion flag
Replace with conversion flag
(RUF010)
214-214: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
236-239: Abstract raise to an inner function
(TRY301)
254-254: Consider moving this statement to an else block
(TRY300)
260-260: Use explicit conversion flag
Replace with conversion flag
(RUF010)
273-273: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
435-438: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
437-437: Use explicit conversion flag
Replace with conversion flag
(RUF010)
440-443: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
442-442: Use explicit conversion flag
Replace with conversion flag
(RUF010)
447-447: Use explicit conversion flag
Replace with conversion flag
(RUF010)
454-454: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
619-622: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
621-621: Use explicit conversion flag
Replace with conversion flag
(RUF010)
624-627: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
626-626: Use explicit conversion flag
Replace with conversion flag
(RUF010)
631-631: Use explicit conversion flag
Replace with conversion flag
(RUF010)
🔇 Additional comments (1)
frontend/app/creator/home/page.tsx (1)
51-65: Header integration looks solidProfileButton slots neatly alongside the existing logout control and keeps state handling untouched. No issues spotted.
frontend/app/brand/profile/page.tsx
Outdated
| <label className="mb-1 block text-sm font-medium text-gray-700">Founded Year</label> | ||
| <input | ||
| type="number" | ||
| value={formData.founded_year || ""} | ||
| onChange={(e) => updateField("founded_year", parseInt(e.target.value) || undefined)} | ||
| disabled={!isEditing} | ||
| className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50" | ||
| /> | ||
| </div> | ||
| <div> | ||
| <label className="mb-1 block text-sm font-medium text-gray-700">Headquarters Location</label> | ||
| <input | ||
| type="text" | ||
| value={formData.headquarters_location || ""} | ||
| onChange={(e) => updateField("headquarters_location", e.target.value)} | ||
| disabled={!isEditing} | ||
| className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50" | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle empty numeric inputs without breaking zero values
Here (and in the other numeric inputs across this page) the parseInt/parseFloat(... ) || … pattern forces zero to become undefined/default values, and even clearing a field can snap back to 1/0 due to the fallback. Please gate on the empty string instead, e.g.:
const raw = e.target.value;
updateField("founded_year", raw === "" ? undefined : parseInt(raw, 10));Apply the same fix to every numeric field so legitimate zero entries persist.
🤖 Prompt for AI Agents
In frontend/app/brand/profile/page.tsx around lines 340 to 357, the current use
of parseInt(...) || undefined (and similar parseFloat(...) || ...) causes valid
zero values to be coerced to undefined and cleared inputs to fallback to
non-empty defaults; change the onChange handlers for all numeric inputs to first
read the raw string value and set undefined when raw === "" otherwise pass the
parsed number (use parseInt(raw, 10) or parseFloat(raw) as appropriate), and
apply this same empty-string gating fix to every numeric field on the page so
zeros are preserved and clearing a field results in undefined.
| type="number" | ||
| value={formData.instagram_followers || ""} | ||
| onChange={(e) => updateField("instagram_followers", parseInt(e.target.value) || undefined)} | ||
| disabled={!isEditing} | ||
| className="w-full rounded-lg border border-gray-300 px-3 py-2 disabled:bg-gray-50" | ||
| /> | ||
| </div> | ||
| <div> | ||
| <label className="mb-1 block text-sm font-medium text-gray-700">YouTube Handle</label> | ||
| <input | ||
| type="text" | ||
| value={formData.youtube_handle || ""} | ||
| onChange={(e) => updateField("youtube_handle", e.target.value)} | ||
| disabled={!isEditing} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t drop legitimate zero values from numeric fields
Using parseInt/parseFloat(... ) || undefined (and similar || fallbacks) means a user-entered 0 becomes falsy and gets replaced with undefined, so we can’t store zero followers, zero budget, etc. The same pattern appears throughout this file. Please switch to an explicit empty-string check so that zero remains zero—for example:
const raw = e.target.value;
updateField("instagram_followers", raw === "" ? undefined : parseInt(raw, 10));Apply the same approach for every integer/decimal field (parseFloat respectively).
🤖 Prompt for AI Agents
In frontend/app/creator/profile/page.tsx around lines 392 to 405, numeric inputs
use expressions like parseInt(e.target.value) || undefined which turn legitimate
0 values into undefined; change each handler to first read const raw =
e.target.value and then call updateField(field, raw === "" ? undefined :
parseInt(raw, 10)) for integer fields (and parseFloat(raw) for decimal fields)
so empty strings map to undefined but numeric zero is preserved; apply this same
explicit empty-string check pattern for every integer/decimal input handler
throughout the file.
| <button | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| className="flex w-full items-center justify-between px-6 py-4 text-left transition hover:bg-gray-50" | ||
| > | ||
| <h3 className="text-lg font-semibold text-gray-900">{title}</h3> | ||
| {isOpen ? ( | ||
| <ChevronUp className="h-5 w-5 text-gray-500" /> | ||
| ) : ( | ||
| <ChevronDown className="h-5 w-5 text-gray-500" /> | ||
| )} | ||
| </button> | ||
| {isOpen && <div className="border-t border-gray-200 px-6 py-4">{children}</div>} | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent collapsible toggle from submitting parent forms.
Line 24 uses a plain <button> inside a component that will live within profile forms. In HTML the default type is "submit", so clicking the collapsible header will fire a form submission instead of just toggling the section. That breaks the editing experience. Please set the button’s type to "button" (and optionally drive aria-expanded) so it behaves purely as a disclosure control.
- <button
- onClick={() => setIsOpen(!isOpen)}
+ <button
+ type="button"
+ onClick={() => setIsOpen((prev) => !prev)}
+ aria-expanded={isOpen}
className="flex w-full items-center justify-between px-6 py-4 text-left transition hover:bg-gray-50"
>🤖 Prompt for AI Agents
In frontend/components/profile/CollapsibleSection.tsx around lines 21 to 33, the
button lacks an explicit type so clicking it inside a form will submit the
parent form; change the button to have type="button" to prevent form submission
and add aria-expanded={isOpen} to reflect its disclosure state for
accessibility. Ensure you keep the onClick handler and existing classes/contents
unchanged and only add the type and aria-expanded attributes.
| const [textValue, setTextValue] = useState( | ||
| value ? JSON.stringify(value, null, 2) : "" | ||
| ); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| const handleBlur = () => { | ||
| if (!textValue.trim()) { | ||
| onChange(null); | ||
| setError(null); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const parsed = JSON.parse(textValue); | ||
| onChange(parsed); | ||
| setError(null); | ||
| } catch (e) { | ||
| setError("Invalid JSON format"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sync local text with incoming value prop
textValue is initialized from value once, but we never update it when the parent supplies fresh data (initial fetch, cancel, AI fill, etc.). That leaves existing JSON fields blank in the UI and risks overwriting server state with an empty object on save. Please watch the value prop and refresh textValue (and clear any stale error) whenever it changes.
- const [textValue, setTextValue] = useState(
- value ? JSON.stringify(value, null, 2) : ""
- );
+ const [textValue, setTextValue] = useState(
+ value ? JSON.stringify(value, null, 2) : ""
+ );
const [error, setError] = useState<string | null>(null);
+
+ useEffect(() => {
+ setTextValue(value ? JSON.stringify(value, null, 2) : "");
+ setError(null);
+ }, [value]);🤖 Prompt for AI Agents
In frontend/components/profile/JsonInput.tsx around lines 18 to 36, textValue is
only initialized from the value prop and not updated when value changes; add a
useEffect that watches value and calls setTextValue(value ?
JSON.stringify(value, null, 2) : "") and setError(null) so the textarea reflects
incoming updates (initial fetch, cancel, AI fill) and clears stale errors.
| <div className="relative h-full w-full overflow-hidden rounded-full bg-gray-200"> | ||
| <Image | ||
| src={profileImageUrl} | ||
| alt={userName || "Profile"} | ||
| fill | ||
| className="object-cover" | ||
| onError={() => { | ||
| setImageError(true); | ||
| setImageLoading(false); | ||
| }} | ||
| onLoad={() => setImageLoading(false)} | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear imageError when the image eventually loads
If the avatar takes longer than three seconds, the timeout flips imageError to true, and we never set it back to false inside onLoad. Slow-but-successful loads will therefore keep showing the fallback monogram forever. Please reset the flag when the image finishes loading.
- onLoad={() => setImageLoading(false)}
+ onLoad={() => {
+ setImageLoading(false);
+ setImageError(false);
+ }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="relative h-full w-full overflow-hidden rounded-full bg-gray-200"> | |
| <Image | |
| src={profileImageUrl} | |
| alt={userName || "Profile"} | |
| fill | |
| className="object-cover" | |
| onError={() => { | |
| setImageError(true); | |
| setImageLoading(false); | |
| }} | |
| onLoad={() => setImageLoading(false)} | |
| /> | |
| </div> | |
| <div className="relative h-full w-full overflow-hidden rounded-full bg-gray-200"> | |
| <Image | |
| src={profileImageUrl} | |
| alt={userName || "Profile"} | |
| fill | |
| className="object-cover" | |
| onError={() => { | |
| setImageError(true); | |
| setImageLoading(false); | |
| }} | |
| onLoad={() => { | |
| setImageLoading(false); | |
| setImageError(false); | |
| }} | |
| /> | |
| </div> |
🤖 Prompt for AI Agents
In frontend/components/profile/ProfileButton.tsx around lines 105 to 117, the
timeout can set imageError to true for slow-but-successful loads and onLoad
currently only clears imageLoading; update the onLoad handler to also reset
imageError to false (i.e., call setImageError(false) before or alongside
setImageLoading(false)) so a late-arriving image will replace the fallback; if
you track a timeout ID elsewhere, also clear that timeout when the image loads
to avoid race conditions.
| export interface BrandProfile { | ||
| id: string; | ||
| user_id: string; | ||
| company_name: string; | ||
| company_tagline?: string; | ||
| company_description?: string; | ||
| company_logo_url?: string; | ||
| company_cover_image_url?: string; | ||
| industry: string; | ||
| sub_industry?: string[]; | ||
| company_size?: string; | ||
| founded_year?: number; | ||
| headquarters_location?: string; | ||
| company_type?: string; | ||
| website_url: string; | ||
| contact_email?: string; | ||
| contact_phone?: string; | ||
| social_media_links?: Record<string, any>; | ||
| target_audience_age_groups?: string[]; | ||
| target_audience_gender?: string[]; | ||
| target_audience_locations?: string[]; | ||
| target_audience_interests?: string[]; | ||
| target_audience_income_level?: string[]; | ||
| target_audience_description?: string; | ||
| brand_values?: string[]; | ||
| brand_personality?: string[]; | ||
| brand_voice?: string; | ||
| brand_colors?: Record<string, any>; | ||
| marketing_goals?: string[]; | ||
| campaign_types_interested?: string[]; | ||
| preferred_content_types?: string[]; | ||
| preferred_platforms?: string[]; | ||
| campaign_frequency?: string; | ||
| monthly_marketing_budget?: number; | ||
| influencer_budget_percentage?: number; | ||
| budget_per_campaign_min?: number; | ||
| budget_per_campaign_max?: number; | ||
| typical_deal_size?: number; | ||
| payment_terms?: string; | ||
| offers_product_only_deals?: boolean; | ||
| offers_affiliate_programs?: boolean; | ||
| affiliate_commission_rate?: number; | ||
| preferred_creator_niches?: string[]; | ||
| preferred_creator_size?: string[]; | ||
| preferred_creator_locations?: string[]; | ||
| minimum_followers_required?: number; | ||
| minimum_engagement_rate?: number; | ||
| content_dos?: string[]; | ||
| content_donts?: string[]; | ||
| brand_safety_requirements?: string[]; | ||
| competitor_brands?: string[]; | ||
| exclusivity_required?: boolean; | ||
| exclusivity_duration_months?: number; | ||
| past_campaigns_count?: number; | ||
| successful_partnerships?: string[]; | ||
| case_studies?: any[]; | ||
| average_campaign_roi?: number; | ||
| products_services?: string[]; | ||
| product_price_range?: string; | ||
| product_categories?: string[]; | ||
| seasonal_products?: boolean; | ||
| product_catalog_url?: string; | ||
| business_verified?: boolean; | ||
| payment_verified?: boolean; | ||
| tax_id_verified?: boolean; | ||
| profile_completion_percentage: number; | ||
| is_active?: boolean; | ||
| is_featured?: boolean; | ||
| is_verified_brand?: boolean; | ||
| subscription_tier?: string; | ||
| featured_until?: string; | ||
| ai_profile_summary?: string; | ||
| search_keywords?: string[]; | ||
| matching_score_base?: number; | ||
| total_deals_posted?: number; | ||
| total_deals_completed?: number; | ||
| total_spent?: number; | ||
| average_deal_rating?: number; | ||
| created_at?: string; | ||
| updated_at?: string; | ||
| last_active_at?: string; | ||
| [key: string]: any; | ||
| } | ||
|
|
||
| export interface CreatorProfile { | ||
| id: string; | ||
| user_id: string; | ||
| display_name: string; | ||
| bio?: string; | ||
| tagline?: string; | ||
| profile_picture_url?: string; | ||
| cover_image_url?: string; | ||
| website_url?: string; | ||
| youtube_url?: string; | ||
| youtube_handle?: string; | ||
| youtube_subscribers?: number; | ||
| instagram_url?: string; | ||
| instagram_handle?: string; | ||
| instagram_followers?: number; | ||
| tiktok_url?: string; | ||
| tiktok_handle?: string; | ||
| tiktok_followers?: number; | ||
| twitter_url?: string; | ||
| twitter_handle?: string; | ||
| twitter_followers?: number; | ||
| twitch_url?: string; | ||
| twitch_handle?: string; | ||
| twitch_followers?: number; | ||
| linkedin_url?: string; | ||
| facebook_url?: string; | ||
| primary_niche: string; | ||
| secondary_niches?: string[]; | ||
| content_types?: string[]; | ||
| content_language?: string[]; | ||
| total_followers: number; | ||
| total_reach?: number; | ||
| average_views?: number; | ||
| engagement_rate?: number; | ||
| audience_age_primary?: string; | ||
| audience_age_secondary?: string[]; | ||
| audience_gender_split?: Record<string, any>; | ||
| audience_locations?: Record<string, any>; | ||
| audience_interests?: string[]; | ||
| average_engagement_per_post?: number; | ||
| posting_frequency?: string; | ||
| best_performing_content_type?: string; | ||
| peak_posting_times?: Record<string, any>; | ||
| years_of_experience?: number; | ||
| content_creation_full_time: boolean; | ||
| team_size: number; | ||
| equipment_quality?: string; | ||
| editing_software?: string[]; | ||
| collaboration_types?: string[]; | ||
| preferred_brands_style?: string[]; | ||
| not_interested_in?: string[]; | ||
| rate_per_post?: number; | ||
| rate_per_video?: number; | ||
| rate_per_story?: number; | ||
| rate_per_reel?: number; | ||
| rate_negotiable: boolean; | ||
| accepts_product_only_deals: boolean; | ||
| minimum_deal_value?: number; | ||
| preferred_payment_terms?: string; | ||
| portfolio_links?: string[]; | ||
| past_brand_collaborations?: string[]; | ||
| case_study_links?: string[]; | ||
| media_kit_url?: string; | ||
| email_verified?: boolean; | ||
| phone_verified?: boolean; | ||
| identity_verified?: boolean; | ||
| profile_completion_percentage: number; | ||
| is_active?: boolean; | ||
| is_featured?: boolean; | ||
| is_verified_creator?: boolean; | ||
| featured_until?: string; | ||
| ai_profile_summary?: string; | ||
| search_keywords?: string[]; | ||
| matching_score_base?: number; | ||
| social_platforms?: Record<string, any>; | ||
| created_at?: string; | ||
| updated_at?: string; | ||
| last_active_at?: string; | ||
| [key: string]: any; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Align profile typings with nullable Supabase fields.
The brand/creator interfaces (e.g., Line 13 company_tagline?: string;, Line 116 secondary_niches?: string[];) model nullable columns as plain string, number, string[], etc. Supabase returns null for unset nullable columns, so at runtime these properties are often null. Because the type system claims they are concrete strings/arrays, downstream code can safely call methods like .trim() or iterate the array and we will crash when the value is actually null. Please widen these optional fields to include null (or introduce a helper like type Nullable<T> = T | null;) so the TypeScript contract matches the API payload.
+type Nullable<T> = T | null;
+
export interface BrandProfile {
id: string;
user_id: string;
- company_tagline?: string;
- company_description?: string;
+ company_tagline?: Nullable<string>;
+ company_description?: Nullable<string>;
...
- sub_industry?: string[];
+ sub_industry?: Nullable<string[]>;
...
- monthly_marketing_budget?: number;
+ monthly_marketing_budget?: Nullable<number>;Please apply the same treatment across the rest of the nullable brand and creator fields.
Committable suggestion skipped: line range outside the PR's diff.
📝 Description
This pull request introduces the profile feature for both brand and creator users. It adds new profile pages, reusable profile components, and the corresponding API client integration for frontend and backend. The backend is updated to support profile-related endpoints.
🔧 Changes Made
backend/app/api/routes/profiles.pybackend/app/main.pyto include profile endpointsfrontend/app/brand/profile/,frontend/app/creator/profile/frontend/components/profile/frontend/lib/api/profile.ts📷 Screenshots or Visual Changes (if applicable)
✅ Checklist
Summary by CodeRabbit