Skip to content
Closed
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
86 changes: 66 additions & 20 deletions Frontend/src/components/Onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
import { Info } from "lucide-react";
Expand Down Expand Up @@ -86,6 +86,23 @@ export default function Onboarding() {
const [brandLogoPreview, setBrandLogoPreview] = useState<string | null>(null);
const [brandError, setBrandError] = useState("");

// Memoized preview URL for profile picture to avoid creating new ObjectURLs every render
const profilePicUrl = useMemo(() => (profilePic ? URL.createObjectURL(profilePic) : null), [profilePic]);
useEffect(() => {
return () => {
if (profilePicUrl) URL.revokeObjectURL(profilePicUrl);
};
}, [profilePicUrl]);

// Revoke brand logo preview when it changes/unmounts
useEffect(() => {
return () => {
if (brandLogoPreview) {
try { URL.revokeObjectURL(brandLogoPreview); } catch {}
}
};
}, [brandLogoPreview]);

// Prefill name and email from Google user if available
useEffect(() => {
if (user) {
Expand Down Expand Up @@ -415,9 +432,9 @@ export default function Onboarding() {
className="hidden"
/>
<div className="flex items-center gap-4 mt-2">
{(profilePic || user?.user_metadata?.avatar_url) ? (
{(profilePicUrl || user?.user_metadata?.avatar_url) ? (
<img
src={profilePic ? URL.createObjectURL(profilePic) : user?.user_metadata?.avatar_url}
src={profilePicUrl ?? user?.user_metadata?.avatar_url}
alt="Profile Preview"
className="h-20 w-20 rounded-full object-cover border-2 border-purple-500"
/>
Expand All @@ -443,26 +460,31 @@ export default function Onboarding() {
// 1. Upload profile picture if provided
if (profilePic) {
setProgress(20);
const fileExt = profilePic.name.split('.').pop();
const fileName = `${user?.id}_${Date.now()}.${fileExt}`;
const { data, error } = await supabase.storage.from('profile-pictures').upload(fileName, profilePic);
const ext = profilePic.name.includes('.') ? profilePic.name.split('.').pop()!.toLowerCase() : undefined;
const fileName = `${user?.id}_${Date.now()}${ext ? `.${ext}` : ''}`;
const { data, error } = await supabase
.storage.from('profile-pictures')
.upload(fileName, profilePic, { contentType: profilePic.type, cacheControl: '3600', upsert: false });
if (error) throw error;
profile_image_url = `${supabase.storage.from('profile-pictures').getPublicUrl(fileName).data.publicUrl}`;
} else if (user?.user_metadata?.avatar_url) {
profile_image_url = user.user_metadata.avatar_url;
}
setProgress(40);
// 2. Update users table
// 2. Ensure auth and upsert users row
const categoryToSave = personal.category === 'Other' ? personal.otherCategory : personal.category;
const { error: userError } = await supabase.from('users').update({
if (!user?.id || !user?.email) throw new Error('You must be signed in to submit onboarding.');
const { error: userError } = await supabase.from('users').upsert({
id: user.id,
email: user.email,
username: personal.name,
age: personal.age,
age: Number(personal.age),
gender: personal.gender,
country: personal.country,
category: categoryToSave,
profile_image: profile_image_url,
role,
}).eq('id', user?.id);
}, { onConflict: 'id' });
if (userError) throw userError;
setProgress(60);
// 3. Insert social_profiles for each platform
Expand Down Expand Up @@ -643,8 +665,14 @@ export default function Onboarding() {
const industries = ["Tech", "Fashion", "Travel", "Food", "Fitness", "Beauty", "Gaming", "Education", "Music", "Finance", "Other"];
const handleBrandLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setBrandData({ ...brandData, logo: e.target.files[0] });
setBrandLogoPreview(URL.createObjectURL(e.target.files[0]));
const file = e.target.files[0];
// Revoke previous preview if present
if (brandLogoPreview) {
try { URL.revokeObjectURL(brandLogoPreview); } catch {}
}
const url = URL.createObjectURL(file);
setBrandData({ ...brandData, logo: file });
setBrandLogoPreview(url);
}
};
const renderBrandDetailsStep = () => (
Expand Down Expand Up @@ -942,11 +970,25 @@ export default function Onboarding() {
setBrandSubmitSuccess("");
let logo_url = null;
try {
if (!user?.id || !user?.email) throw new Error("Please sign in to finish brand onboarding.");
// 0. Ensure user exists in users table (upsert)
if (user) {
const upsertUser = {
id: user.id,
email: user.email,
username: user.user_metadata?.name || user.email || `user_${user.id}`,
role: 'brand',
};
const { error: upsertError } = await supabase.from('users').upsert(upsertUser, { onConflict: 'id' });
if (upsertError) throw upsertError;
}
// 1. Upload logo if provided
if (brandData.logo) {
const fileExt = brandData.logo.name.split('.').pop();
const fileName = `${user?.id}_${Date.now()}.${fileExt}`;
const { data, error } = await supabase.storage.from('brand-logos').upload(fileName, brandData.logo);
if (brandData.logo instanceof File) {
const ext = brandData.logo.name.includes('.') ? brandData.logo.name.split('.').pop()!.toLowerCase() : undefined;
const fileName = `${user!.id}_${Date.now()}${ext ? `.${ext}` : ''}`;
const { data, error } = await supabase
.storage.from('brand-logos')
.upload(fileName, brandData.logo, { contentType: brandData.logo.type, cacheControl: '3600', upsert: false });
if (error) throw error;
logo_url = supabase.storage.from('brand-logos').getPublicUrl(fileName).data.publicUrl;
}
Expand Down Expand Up @@ -992,8 +1034,8 @@ export default function Onboarding() {
<h2 className="text-2xl font-bold mb-4">Review & Submit</h2>
<div className="mb-4">
<label className="block font-medium mb-2">Logo</label>
{(brandLogoPreview || brandData.logo) ? (
<img src={brandLogoPreview || (brandData.logo ? URL.createObjectURL(brandData.logo) : undefined)} alt="Logo Preview" className="h-16 w-16 rounded-full object-cover border mb-2" />
{(brandLogoPreview || (brandData.logo instanceof File ? brandLogoPreview : undefined)) ? (
<img src={(brandLogoPreview || (brandData.logo instanceof File ? brandLogoPreview : undefined)) ?? ''} alt="Logo Preview" className="h-16 w-16 rounded-full object-cover border mb-2" />
) : (
<div className="h-16 w-16 rounded-full bg-gray-200 flex items-center justify-center text-gray-400">No Logo</div>
)}
Expand Down Expand Up @@ -1056,7 +1098,11 @@ export default function Onboarding() {
const savedStep = localStorage.getItem("brandStep");
const savedData = localStorage.getItem("brandData");
if (savedStep) setBrandStep(Number(savedStep));
if (savedData) setBrandData(JSON.parse(savedData));
if (savedData) {
const parsed = JSON.parse(savedData);
if (parsed.logo) parsed.logo = null;
setBrandData(parsed);
}
}, []);
useEffect(() => {
localStorage.setItem("brandStep", String(brandStep));
Expand Down Expand Up @@ -1215,7 +1261,7 @@ function YouTubeDetails({ details, setDetails }: { details: any, setDetails: (d:

return (
<div className="space-y-2">
<label className="block font-medium flex items-center gap-2">
<label className="flex font-medium items-center gap-2">
YouTube Channel URL or ID
<button
type="button"
Expand Down
196 changes: 196 additions & 0 deletions Frontend/src/pages/Profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import React, { useEffect, useMemo, useState } from "react";
import { userApi, UserProfile } from "../services/userApi";
import { useAuth } from "../context/AuthContext";

const container: React.CSSProperties = {
maxWidth: 840,
margin: "40px auto",
padding: 24,
background: "rgba(26,26,26,0.6)",
border: "1px solid rgba(255,255,255,0.08)",
borderRadius: 16,
backdropFilter: "blur(16px)",
color: "#fff",
};

const field: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 8, marginBottom: 16 };
const label: React.CSSProperties = { fontSize: 13, color: "#a0a0a0" };
const inputBase: React.CSSProperties = {
background: "#121212",
border: "1px solid #2a2a2a",
color: "#fff",
borderRadius: 10,
padding: "10px 12px",
};
const actions: React.CSSProperties = { display: "flex", gap: 12, justifyContent: "flex-end", marginTop: 20 };

export default function ProfilePage() {
const { user } = useAuth();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [profile, setProfile] = useState<UserProfile | null>(null);
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [bio, setBio] = useState("");
const [role, setRole] = useState<string | undefined>(undefined);
const [imageFile, setImageFile] = useState<File | null>(null);

const disabled = useMemo(() => saving || loading, [saving, loading]);

useEffect(() => {
let mounted = true;
(async () => {
try {
setLoading(true);
const p = await userApi.getProfile();
if (!mounted) return;
setProfile(p);
setUsername(p.username ?? "");
setEmail(p.email ?? user?.email ?? "");
setBio(p.bio ?? "");
setRole((p.role ?? undefined) as string | undefined);
setError(null);
} catch (e: any) {
if (!mounted) return;
setError(e?.message || "Failed to load profile");
} finally {
if (mounted) setLoading(false);
}
})();
return () => { mounted = false; };
}, [user?.id]);

const onSelectImage: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const f = e.target.files?.[0] || null;
if (!f) return setImageFile(null);
if (!f.type.startsWith("image/")) { setError("Please select an image file"); return; }
if (f.size > 5 * 1024 * 1024) { setError("Max file size is 5MB"); return; }
setError(null);
setImageFile(f);
};

const onSave = async () => {
try {
setSaving(true);
setError(null);

// 1) Upload image first if any (backend returns updated profile)
if (imageFile) {
const updatedAfterImage = await userApi.uploadProfileImage(imageFile);
setProfile(updatedAfterImage);
}

// 2) Update profile fields
const payload: Partial<UserProfile> = {
username: username.trim() || undefined,
bio: bio.trim() || undefined,
// role is generally set during onboarding; do not allow arbitrary changes here unless present
role: role as any,
// profile_image_url already updated if image was uploaded above
};
const updated = await userApi.updateProfile(payload);
setProfile(updated);
if (updated.username) setUsername(updated.username);
if (updated.email) setEmail(updated.email);
if (updated.bio) setBio(updated.bio);
if (updated.role) setRole(updated.role);
if (updated.profile_image_url) setImageFile(null);
} catch (e: any) {
setError(e?.message || "Failed to save profile");
} finally {
setSaving(false);
}
};

return (
<div style={container}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16 }}>
<h1 style={{ margin: 0, fontSize: 20 }}>Your Profile</h1>
{loading && <span style={{ color: "#a0a0a0", fontSize: 12 }}>Loading…</span>}
</div>

{error && (
<div style={{
marginBottom: 16,
background: "#2a1e1e",
border: "1px solid #5a2a2a",
color: "#ffbdbd",
borderRadius: 10,
padding: 12,
fontSize: 13,
}}>{error}</div>
)}

{/* Avatar */}
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 16 }}>
<div style={{ width: 80, height: 80, borderRadius: "50%", overflow: "hidden", background: "#222", border: "1px solid #2a2a2a" }}>
{profile?.profile_image_url ? (
<img src={profile.profile_image_url} alt="avatar" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
) : (
<div style={{ width: "100%", height: "100%" }} />
)}
</div>
<label style={{ ...inputBase, display: "inline-block", cursor: "pointer" }}>
<input type="file" accept="image/*" onChange={onSelectImage} style={{ display: "none" }} />
{imageFile ? `Selected: ${imageFile.name}` : "Upload new picture"}
</label>
</div>

{/* Fields */}
<div style={field}>
<label style={label}>Username</label>
<input
style={inputBase}
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Your display name"
disabled={disabled}
/>
</div>

<div style={field}>
<label style={label}>Email</label>
<input style={{ ...inputBase, opacity: 0.7 }} type="email" value={email} disabled />
</div>

<div style={field}>
<label style={label}>Bio</label>
<textarea
style={{ ...inputBase, minHeight: 100, resize: "vertical" }}
value={bio}
onChange={(e) => setBio(e.target.value)}
placeholder="Tell others about you"
disabled={disabled}
/>
</div>

{!!role && (
<div style={field}>
<label style={label}>Role</label>
<input style={{ ...inputBase, opacity: 0.7 }} value={role} disabled />
</div>
)}

<div style={actions}>
<button
onClick={onSave}
disabled={disabled}
style={{
background: "#0B00CF",
border: "none",
color: "#fff",
borderRadius: 10,
padding: "10px 16px",
cursor: disabled ? "not-allowed" : "pointer",
opacity: disabled ? 0.7 : 1,
fontWeight: 600,
}}
>
{saving ? "Saving…" : "Save Changes"}
</button>
</div>
</div>
);
}