From a0559a17f1ec8b925d6e7b8dedfd2360366ab742 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sat, 24 Jan 2026 22:19:30 +0530 Subject: [PATCH] fix --- next.config.ts | 8 + src/app/(agent)/agent/dashboard/page.tsx | 178 +++++++++++--- src/app/(agent)/agent/edit/page.tsx | 25 +- src/components/profile/PhotoUploadModal.tsx | 251 ++++++++++++++++++++ src/components/profile/index.ts | 1 + src/services/upload.service.ts | 31 +++ 6 files changed, 454 insertions(+), 40 deletions(-) create mode 100644 src/components/profile/PhotoUploadModal.tsx diff --git a/next.config.ts b/next.config.ts index dc2d4fe..1ef3db0 100644 --- a/next.config.ts +++ b/next.config.ts @@ -11,6 +11,14 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "**", }, + { + protocol: "http", + hostname: "localhost", + }, + { + protocol: "http", + hostname: "127.0.0.1", + }, ], }, }; diff --git a/src/app/(agent)/agent/dashboard/page.tsx b/src/app/(agent)/agent/dashboard/page.tsx index 7af823f..3dd86ef 100644 --- a/src/app/(agent)/agent/dashboard/page.tsx +++ b/src/app/(agent)/agent/dashboard/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState, useEffect } from 'react'; import { useSession } from 'next-auth/react'; import Image from 'next/image'; @@ -12,20 +13,13 @@ import { TestimonialsSection, StatusButtons, ContactInfo, + PhotoUploadModal, } from '@/components/profile'; +import { agentsService, AgentProfile } from '@/services/agents.service'; +import { uploadService } from '@/services/upload.service'; -// Mock agent data - in production, this would come from API -const agentData = { - name: 'Brian Neeland', - isVerified: true, - title: 'Licensed Real Estate Agent', - location: 'Colorado', - memberSince: 'March 2020', - bio: 'Brian brings eight years of hands-on experience helping clients confidently buy, sell, and invest in real estate. He combines strong analytical skills with deep market knowledge to identify the right opportunities and guide clients through every step of the process. With a data-driven approach and clear communication, Brian ensures smooth transactions and informed decisions tailored to each client\'s goals.', - expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'condo', 'Rural', 'FHA', 'Conventional', 'USDA', 'Retirement'], - email: 'brian@gmail.com', - phone: '+91*******7493', - profileImage: '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg', +// Mock data for sections not yet available from API +const mockData = { experience: { years: '10+ years', contracts: '10+ Contracts', @@ -81,7 +75,106 @@ const agentData = { export default function AgentDashboard() { const { data: session } = useSession(); - const [firstName, lastName] = agentData.name.split(' '); + const [agentProfile, setAgentProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isPhotoModalOpen, setIsPhotoModalOpen] = useState(false); + const [imageError, setImageError] = useState(false); + + const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg'; + + // Fetch agent profile on mount + useEffect(() => { + const fetchProfile = async () => { + try { + setLoading(true); + setError(null); + const profile = await agentsService.getMyProfile(); + setAgentProfile(profile); + } catch (err) { + console.error('Failed to fetch profile:', err); + setError('Failed to load profile data'); + } finally { + setLoading(false); + } + }; + + fetchProfile(); + }, []); + + const handlePhotoUpload = async (file: File) => { + // Upload avatar to S3 (uses agent ID as filename for auto-replacement) + const uploadedFile = await uploadService.uploadAvatar(file); + + // Update agent profile with the S3 URL + const updatedProfile = await agentsService.updateProfile({ avatar: uploadedFile.url }); + setImageError(false); // Reset error state for new image + setAgentProfile(updatedProfile); + }; + + const getProfileImageUrl = () => { + // If image failed to load, return default + if (imageError) { + return defaultImage; + } + + // Check for null, undefined, or empty string + if (!agentProfile?.avatar || agentProfile.avatar.trim() === '') { + return defaultImage; + } + + // S3 URLs are full URLs starting with http + if (agentProfile.avatar.startsWith('http')) { + return agentProfile.avatar; + } + + // For relative paths (local assets), return as-is + return agentProfile.avatar; + }; + + if (loading) { + return ( +
+
+
+

Loading profile...

+
+
+ ); + } + + if (error || !agentProfile) { + return ( +
+
+
+ + + +
+

Unable to Load Profile

+

{error || 'Profile not found'}

+ +
+
+ ); + } + + // Format member since date + const formatMemberSince = (dateString?: string) => { + if (!dateString) return 'Member'; + try { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); + } catch { + return 'Member'; + } + }; return (
@@ -92,18 +185,24 @@ export default function AgentDashboard() { {/* Profile Image */}
- Profile { + const target = e.target as HTMLImageElement; + target.src = defaultImage; + }} /> {/* Gradient Overlay */}
{/* Edit Icon - Top Right Outside */} -
{/* Right Content - Profile Info + Experience + All Sections */}
{/* Profile Card */} {/* Experience Section */} - + {/* Info Cards Section */}
@@ -155,9 +257,9 @@ export default function AgentDashboard() { } content={
-

{agentData.availability.type}

+

{mockData.availability.type}

- {agentData.availability.days.map((day) => ( + {mockData.availability.days.map((day) => (

{day}

))}
@@ -174,7 +276,7 @@ export default function AgentDashboard() { height={28} /> } - content={

{agentData.preferredWorkEnvironment}

} + content={

{mockData.preferredWorkEnvironment}

} /> } - content={

“{agentData.testimonialHighlight}”

} + content={

“{mockData.testimonialHighlight}”

} />
{/* Specialization Section */} - + {/* Testimonials Section */} - +
+ + {/* Photo Upload Modal */} + setIsPhotoModalOpen(false)} + onUpload={handlePhotoUpload} + currentPhoto={agentProfile.avatar} + />
); } diff --git a/src/app/(agent)/agent/edit/page.tsx b/src/app/(agent)/agent/edit/page.tsx index 07d27c2..9ed49a0 100644 --- a/src/app/(agent)/agent/edit/page.tsx +++ b/src/app/(agent)/agent/edit/page.tsx @@ -53,6 +53,9 @@ export default function EditProfilePage() { setSections(sortedSections); + // Debug: Log all sections and their isRepeatable status + console.log('All sections:', sortedSections.map(s => ({ slug: s.slug, name: s.name, isRepeatable: s.isRepeatable }))); + // Initialize form data with existing profile data + default values from fields const initialData: Record = {}; @@ -100,7 +103,6 @@ export default function EditProfilePage() { // Fetch existing field values from the API try { const fieldValuesResponse = await agentsService.getFieldValues(); - console.log('Field values from API:', fieldValuesResponse.fieldValues); if (fieldValuesResponse.fieldValues) { fieldValuesResponse.fieldValues.forEach(fv => { // Only set if value exists and is not null/undefined @@ -114,8 +116,6 @@ export default function EditProfilePage() { console.log('No existing field values found, using defaults'); } - console.log('initialData after field values:', initialData); - setFormData(initialData); // Initialize repeatable sections data @@ -124,9 +124,7 @@ export default function EditProfilePage() { if (section.isRepeatable) { // Check if there's existing data for this section (stored as `${sectionSlug}_entries`) const entriesKey = `${section.slug}_entries`; - console.log(`Checking repeatable section: ${section.slug}, entriesKey: ${entriesKey}`); const existingEntries = initialData[entriesKey]; - console.log(`Existing entries for ${entriesKey}:`, existingEntries); if (Array.isArray(existingEntries) && existingEntries.length > 0) { initialRepeatableData[section.slug] = existingEntries as RepeatableEntryData[]; } else { @@ -135,7 +133,22 @@ export default function EditProfilePage() { } } }); - console.log('initialRepeatableData:', initialRepeatableData); + + // Also check for any _entries fields that might not match a repeatable section + // This handles cases where data was saved but section isn't marked as repeatable + Object.keys(initialData).forEach(key => { + if (key.endsWith('_entries') && Array.isArray(initialData[key])) { + const sectionSlug = key.replace('_entries', ''); + if (!initialRepeatableData[sectionSlug]) { + // Find the section by slug + const section = sortedSections.find(s => s.slug === sectionSlug); + if (section) { + initialRepeatableData[sectionSlug] = initialData[key] as RepeatableEntryData[]; + } + } + } + }); + setRepeatableData(initialRepeatableData); } catch (err: unknown) { diff --git a/src/components/profile/PhotoUploadModal.tsx b/src/components/profile/PhotoUploadModal.tsx new file mode 100644 index 0000000..2447ee1 --- /dev/null +++ b/src/components/profile/PhotoUploadModal.tsx @@ -0,0 +1,251 @@ +'use client'; + +import { useState, useRef, useCallback } from 'react'; +import Image from 'next/image'; + +interface PhotoUploadModalProps { + isOpen: boolean; + onClose: () => void; + onUpload: (file: File) => Promise; + currentPhoto?: string | null; +} + +export function PhotoUploadModal({ isOpen, onClose, onUpload, currentPhoto }: PhotoUploadModalProps) { + const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + const validateFile = (file: File): string | null => { + const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; + const maxSize = 5 * 1024 * 1024; // 5MB + + if (!allowedTypes.includes(file.type)) { + return 'Only JPG, PNG, GIF, and WebP images are allowed'; + } + + if (file.size > maxSize) { + return 'File size must be less than 5MB'; + } + + return null; + }; + + const handleFileSelect = (file: File) => { + const validationError = validateFile(file); + if (validationError) { + setError(validationError); + return; + } + + setError(null); + setSelectedFile(file); + + // Create preview URL + const reader = new FileReader(); + reader.onload = (e) => { + setPreviewUrl(e.target?.result as string); + }; + reader.readAsDataURL(file); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + handleFileSelect(file); + } + }; + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + + const file = e.dataTransfer.files?.[0]; + if (file) { + handleFileSelect(file); + } + }, []); + + const handleUpload = async () => { + if (!selectedFile) return; + + try { + setIsUploading(true); + setError(null); + await onUpload(selectedFile); + handleClose(); + } catch (err) { + console.error('Upload failed:', err); + setError('Failed to upload photo. Please try again.'); + } finally { + setIsUploading(false); + } + }; + + const handleClose = () => { + setSelectedFile(null); + setPreviewUrl(null); + setError(null); + setIsDragging(false); + onClose(); + }; + + const handleBrowseClick = () => { + fileInputRef.current?.click(); + }; + + if (!isOpen) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+

Upload Profile Photo

+ +
+ + {/* Content */} +
+ {/* Error Message */} + {error && ( +
+ + + +

{error}

+
+ )} + + {/* Preview or Upload Area */} + {previewUrl ? ( +
+
+ Preview +
+ +
+ ) : ( +
+ {/* Current Photo Preview */} + {currentPhoto && ( +
+
+ Current photo +
+
+ )} + +
+
+ + + +
+
+

+ Drag and drop your photo here +

+

+ or{' '} + +

+
+

+ JPG, PNG, GIF, WebP • Max 5MB +

+
+ + +
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts index b0bf67e..27c3980 100644 --- a/src/components/profile/index.ts +++ b/src/components/profile/index.ts @@ -10,3 +10,4 @@ export { TestimonialCard } from './TestimonialCard'; export { TestimonialsSection } from './TestimonialsSection'; export { StatusButtons } from './StatusButtons'; export { ContactInfo } from './ContactInfo'; +export { PhotoUploadModal } from './PhotoUploadModal'; diff --git a/src/services/upload.service.ts b/src/services/upload.service.ts index 9e0f8ba..f91c04d 100644 --- a/src/services/upload.service.ts +++ b/src/services/upload.service.ts @@ -146,6 +146,37 @@ class UploadService { const encodedKey = encodeURIComponent(fileKey); await api.delete(`${this.basePath}/${encodedKey}`); } + + /** + * Upload avatar image (uses agent profile ID as filename for auto-replacement) + */ + async uploadAvatar( + file: File, + onProgress?: (progress: UploadProgress) => void + ): Promise { + // Step 1: Get presigned URL from backend (uses agent ID as filename) + const response = await api.post<{ data: PresignedUrlResponse }>( + `${this.basePath}/avatar-presigned-url`, + { + filename: file.name, + contentType: file.type, + } + ); + const { uploadUrl, publicUrl, key } = response.data.data; + + // Step 2: Upload directly to S3 + await this.uploadToS3(uploadUrl, file, onProgress); + + // Step 3: Return uploaded file info + return { + id: key, + name: file.name, + size: file.size, + type: file.type, + url: publicUrl, + uploadedAt: new Date().toISOString(), + }; + } } export const uploadService = new UploadService();