From 6b502ffc804b627261600fd47e3251866916ceda Mon Sep 17 00:00:00 2001 From: Priyans00 <115811175+Priyans00@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:48:15 +0530 Subject: [PATCH 1/3] Fix: Fix profile-login --- app/api/profile/route.ts | 64 +++- app/profile/profile-component.tsx | 602 +++++++++++++++++++++++------- setup.sql | 463 ++++++----------------- 3 files changed, 634 insertions(+), 495 deletions(-) diff --git a/app/api/profile/route.ts b/app/api/profile/route.ts index eabd7c1..89faf12 100644 --- a/app/api/profile/route.ts +++ b/app/api/profile/route.ts @@ -1,26 +1,41 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@/lib/supabase/server"; -export async function GET() { +export async function GET(request: NextRequest) { const supabase = await createClient(); try { - // Get the authenticated user - const { data: { user }, error: authError } = await supabase.auth.getUser(); - - if (authError || !user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + // Try to get user ID from multiple sources + let userId: string | null = null; + let userEmail: string | null = null; + + // First, try Supabase auth (for existing sessions) + const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser(); + if (supabaseUser && !authError) { + userId = supabaseUser.id; + userEmail = supabaseUser.email || null; + } else { + // If no Supabase session, try to get from request headers or query + const userIdFromHeader = request.headers.get('X-User-ID'); + const userIdFromQuery = request.nextUrl.searchParams.get('userId'); + + userId = userIdFromHeader || userIdFromQuery; + + if (!userId) { + return NextResponse.json({ error: "No user session found. Please log in again." }, { status: 401 }); + } } - // Get user profile + // Get user profile using the userId const { data: profile, error: profileError } = await supabase .from('user_profiles') .select('*') - .eq('id', user.id) + .eq('id', userId) .single(); if (profileError) { - return NextResponse.json({ error: profileError.message }, { status: 500 }); + console.error('Profile error:', profileError); + return NextResponse.json({ error: "Profile not found. Please try logging in again." }, { status: 404 }); } // Get user's items @@ -30,7 +45,7 @@ export async function GET() { *, categories (name) `) - .eq('seller_id', user.id) + .eq('seller_id', userId) .order('created_at', { ascending: false }); if (itemsError) { @@ -67,7 +82,7 @@ export async function GET() { const responseData = { profile: { ...profile, - email: user.email + email: userEmail || profile.email }, items: itemsWithStats, stats: { @@ -93,15 +108,27 @@ export async function PUT(req: NextRequest) { const supabase = await createClient(); try { - // Get the authenticated user - const { data: { user }, error: authError } = await supabase.auth.getUser(); - - if (authError || !user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + // Try to get user ID from multiple sources + let userId: string | null = null; + + // First, try Supabase auth (for existing sessions) + const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser(); + if (supabaseUser && !authError) { + userId = supabaseUser.id; + } else { + // If no Supabase session, try to get from request headers or query + const userIdFromHeader = req.headers.get('X-User-ID'); + const userIdFromQuery = req.nextUrl.searchParams.get('userId'); + + userId = userIdFromHeader || userIdFromQuery; + + if (!userId) { + return NextResponse.json({ error: "No user session found. Please log in again." }, { status: 401 }); + } } const body = await req.json(); - const { name, bio, phone, year_of_study, branch, location } = body; + const { name, bio, phone, nickname, year_of_study, branch, location } = body; // Update user profile const { data, error } = await supabase @@ -110,12 +137,13 @@ export async function PUT(req: NextRequest) { name, bio, phone, + nickname, year_of_study, branch, location, updated_at: new Date().toISOString() }) - .eq('id', user.id) + .eq('id', userId) .select() .single(); diff --git a/app/profile/profile-component.tsx b/app/profile/profile-component.tsx index cf9a820..97f3d3f 100644 --- a/app/profile/profile-component.tsx +++ b/app/profile/profile-component.tsx @@ -1,93 +1,255 @@ "use client"; -import { useState, useEffect } from 'react'; -import { createClient } from '@/lib/supabase/client'; +import { useState, useEffect, useMemo, useCallback } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; -import { User as UserIcon, Mail, Calendar, Star } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Mail, + Calendar, + Star, + Edit3, + Phone, + MapPin, + Eye, + Heart, + Package, + ShoppingBag, +} from "lucide-react"; +import Link from "next/link"; +import { useAuth } from "@/contexts/auth-context"; +import { getDisplayName, getDisplayInitials } from "@/lib/utils"; +import Image from "next/image"; -interface UserProfile { +interface UserStats { + totalItemsSold: number; + totalItemsBought: number; + totalViews: number; + totalLikes: number; + averageRating: number; +} + +interface UserItem { id: string; - name: string | null; - email: string | null; - srn: string | null; - phone: string | null; - bio: string | null; - program: string | null; - branch: string | null; - semester: string | null; - rating: number; - verified: boolean; - location: string | null; + title: string; + price: number; + condition: string; + category: string; + images: string[]; + views: number; + likes: number; created_at: string; + is_available: boolean; } -interface UserStats { - items_sold: number; - items_bought: number; - total_items: number; - rating: number; +interface ProfileData { + profile: { + id: string; + name: string; + srn: string; + nickname: string | null; + bio: string | null; + phone: string | null; + rating: number; + verified: boolean; + location: string | null; + year_of_study: number | null; + branch: string | null; + created_at: string; + email?: string; + }; + items: UserItem[]; + stats: UserStats; } export function ProfileComponent() { - const [profile, setProfile] = useState(null); - const [stats, setStats] = useState(null); - const [loading, setLoading] = useState(true); + const { user } = useAuth(); + const [profileData, setProfileData] = useState(null); + const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + const [editBio, setEditBio] = useState(""); + const [editPhone, setEditPhone] = useState(""); + const [editNickname, setEditNickname] = useState(""); - useEffect(() => { - async function loadProfile() { - try { - const supabase = createClient(); - const { data: { user }, error: userError } = await supabase.auth.getUser(); - - if (userError || !user) { - setError('Not authenticated'); - return; - } + // Memoized currency formatter + const formatCurrency = useMemo(() => { + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }); + }, []); + + const fetchProfileData = useCallback(async () => { + if (!user) { + setError("Please log in to view your profile."); + setIsLoading(false); + return; + } - // Fetch profile - const { data: profileData, error: profileError } = await supabase - .from('user_profiles') - .select('*') - .eq('id', user.id) - .single(); + try { + setIsLoading(true); + setError(null); - if (profileError) { - console.error('Profile error:', profileError); - setError('Failed to load profile'); + // Check cache first + const cacheKey = `profile_data_${user.id}`; + const cached = localStorage.getItem(cacheKey); + if (cached) { + const parsedCache = JSON.parse(cached); + const cacheAge = Date.now() - parsedCache.timestamp; + // Use cache if less than 5 minutes old + if (cacheAge < 5 * 60 * 1000) { + setProfileData(parsedCache.data); + setEditBio(parsedCache.data.profile.bio || ""); + setEditPhone(parsedCache.data.profile.phone || ""); + setEditNickname(parsedCache.data.profile.nickname || ""); + setIsLoading(false); return; } + } - setProfile(profileData); + const response = await fetch(`/api/profile?userId=${user.id}`, { + headers: { + 'X-User-ID': user.id, + } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + const data: ProfileData = await response.json(); + + // Cache the data + localStorage.setItem(cacheKey, JSON.stringify({ + data, + timestamp: Date.now() + })); + + setProfileData(data); + setEditBio(data.profile.bio || ""); + setEditPhone(data.profile.phone || ""); + setEditNickname(data.profile.nickname || ""); + } catch (error) { + console.error('Error fetching profile data:', error); + setError(error instanceof Error ? error.message : 'Failed to load profile'); + } finally { + setIsLoading(false); + } + }, [user]); - // Fetch stats - const { data: itemsData } = await supabase - .from('items') - .select('id, is_available') - .eq('seller_id', user.id); + useEffect(() => { + if (user) { + fetchProfileData(); + } else { + setError("Please log in to view your profile."); + setIsLoading(false); + } + }, [user, fetchProfileData]); - const stats: UserStats = { - items_sold: itemsData?.filter(item => !item.is_available).length || 0, - items_bought: 0, // Would need a purchases or transactions table - total_items: itemsData?.length || 0, - rating: profileData?.rating || 0 - }; + const handleUpdateProfile = async () => { + if (!profileData || !user) return; + + // Input validation + const bioTrimmed = editBio.trim(); + const phoneTrimmed = editPhone.trim(); + const nicknameTrimmed = editNickname.trim(); - setStats(stats); - } catch (err) { - console.error('Error loading profile:', err); - setError('Failed to load profile'); - } finally { - setLoading(false); - } + if (bioTrimmed.length > 500) { + setError('Bio must be 500 characters or less'); + return; } - loadProfile(); - }, []); + if (phoneTrimmed && !/^\+?[\d\s\-\(\)]{10,15}$/.test(phoneTrimmed)) { + setError('Please enter a valid phone number'); + return; + } - if (loading) { + if (nicknameTrimmed && (nicknameTrimmed.length < 2 || nicknameTrimmed.length > 50)) { + setError('Nickname must be between 2 and 50 characters'); + return; + } + + // Check for potentially inappropriate content in nickname + const inappropriateWords = ['admin', 'moderator', 'official', 'pesu', 'university']; + if (nicknameTrimmed && inappropriateWords.some(word => + nicknameTrimmed.toLowerCase().includes(word.toLowerCase()) + )) { + setError('Please choose a different nickname'); + return; + } + + try { + setIsUpdating(true); + setError(null); + + const response = await fetch(`/api/profile?userId=${user.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-User-ID': user.id, + }, + body: JSON.stringify({ + bio: bioTrimmed || null, + phone: phoneTrimmed || null, + nickname: nicknameTrimmed || null, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to update profile'); + } + + const result = await response.json(); + + // Update the profile data with the new values + if (profileData) { + const updatedProfileData: ProfileData = { + ...profileData, + profile: { + ...profileData.profile, + bio: result.bio, + phone: result.phone, + nickname: result.nickname, + } + }; + setProfileData(updatedProfileData); + + // Update cache + const cacheKey = `profile_data_${user.id}`; + localStorage.setItem(cacheKey, JSON.stringify({ + data: updatedProfileData, + timestamp: Date.now() + })); + } + + setIsDialogOpen(false); + } catch (error) { + console.error('Error updating profile:', error); + setError(error instanceof Error ? error.message : 'Failed to update profile'); + } finally { + setIsUpdating(false); + } + }; + + if (isLoading) { return (
@@ -123,112 +285,276 @@ export function ProfileComponent() {

{error}

+
); } + if (!profileData) { + return null; + } + return ( -
- - -
-
- -
-
- {profile?.name || 'User'} - - - {profile?.email} - {profile?.srn && ( - PESU Student +
+
+ {/* Profile Card */} +
+ + +
+
+ {getDisplayInitials(profileData.profile)} +
+
+ +
+ {getDisplayName(profileData.profile)} + {profileData.profile.nickname && ( + + ({profileData.profile.name}) + + )} +
+ + Verified PESU Student + +
+ + {profileData.profile.bio || "No bio available"} + +
+ +
+
+ + {profileData.profile.email} +
+ {profileData.profile.phone && ( +
+ + {profileData.profile.phone} +
)} - {profile?.verified && ( - Verified + {profileData.profile.location && ( +
+ + {profileData.profile.location} +
)} - -
-
- - - {/* Stats */} -
+
+ + + Member since {new Date(profileData.profile.created_at).toLocaleDateString()} + +
+
+ + {profileData.profile.rating.toFixed(1)} rating +
+
+ +
+ + + + + + + Edit Profile + + Update your display name, bio and contact information. Use a nickname for enhanced privacy. + + +
+ {error && ( +
+ {error} +
+ )} +
+ + setEditNickname(e.target.value)} + maxLength={50} + /> +
+ This will be shown instead of your real name. Leave empty to use your real name. +
+
+
+ +