import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import Navbar from './Navbar';
import Footer from './footer';
import './ProfilePage.css';
const ProfilePage = () => {
const navigate = useNavigate();
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [isImageLoaded, setIsImageLoaded] = useState(false);
const [newComment, setNewComment] = useState('');
const [userData, setUserData] = useState({
username: '',
bio: 'No bio yet',
profilePicture: '',
posts: [],
followers: [],
following: [],
postsCount: 0,
});
const [showNewPostModal, setShowNewPostModal] = useState(false);
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
const [showPostDetailModal, setShowPostDetailModal] = useState(false);
const [selectedPost, setSelectedPost] = useState(null);
const [likedPosts, setLikedPosts] = useState([]);
const [showFollowModal, setShowFollowModal] = useState(false);
const [followListType, setFollowListType] = useState(''); // 'followers' or 'following'
const [followList, setFollowList] = useState([]);
const [isLoadingFollowList, setIsLoadingFollowList] = useState(false);
const [newPost, setNewPost] = useState({
image: null,
description: '',
tags: '',
public: false,
pin_id: null,
likes: 0,
comments: {},
});
const [editProfile, setEditProfile] = useState({
bio: '',
profile_picture: null
});
const [isUploading, setIsUploading] = useState(false);
const [pins, setPins] = useState([]);
// Default profile picture URL
const defaultProfilePic = 'https://journeypoint-bucket.s3.us-east-2.amazonaws.com/uploads/default_profile_pic.jpg';
// Check login status and fetch data
useEffect(() => {
const storedUsername = localStorage.getItem('username');
const loginStatus = localStorage.getItem('login');
if (loginStatus === 'true' && storedUsername) {
setIsLoggedIn(true);
setUserData(prev => ({
...prev,
username: storedUsername,
bio: 'No bio yet',
profilePicture: defaultProfilePic
}));
fetchUserData(storedUsername);
fetchUserPosts(storedUsername);
fetchUserPins(storedUsername);
} else {
navigate('/login');
}
}, [navigate]);
// Handle post click
const handlePostClick = (post) => {
setSelectedPost(post);
setShowPostDetailModal(true);
};
// Fetch user profile data
const fetchUserData = async (username) => {
try {
const response = await fetch(`https://jp-backend-kc80.onrender.com/api/getuser?username=${username}`);
const user = await response.json();
if (user.length > 0) {
setUserData(prev => ({
...prev,
username: user[0].username,
bio: user[0].bio || 'No bio yet',
profilePicture: user[0].profile_picture,
followers: user[0].followers || [],
following: user[0].following || [],
}));
}
} catch (error) {
console.error('Error fetching user data:', error);
}
};
// Fetch user posts
const fetchUserPosts = async (username) => {
try {
const response = await fetch(`https://jp-backend-kc80.onrender.com/api/userposts?username=${username}`);
const data = await response.json();
setUserData(prev => ({
...prev,
posts: Array.isArray(data) ? data : [],
postsCount: Array.isArray(data) ? data.length : 0
}));
} catch (error) {
console.error('Error fetching user posts:', error);
}
};
// Fetch user pins
const fetchUserPins = async (username) => {
try {
const response = await fetch(`https://jp-backend-kc80.onrender.com/api/pins?username=${username}`);
const data = await response.json();
setPins(Array.isArray(data) ? data : []);
} catch (error) {
console.error('Error fetching pins:', error);
}
};
const showFollowList = async (type) => {
setFollowListType(type);
setIsLoadingFollowList(true);
setShowFollowModal(true);
try {
// Get the appropriate list of usernames
const usernames = type === 'followers' ? userData.followers : userData.following;
// Fetch details for each user in parallel
const usersData = await Promise.all(
usernames.map(async (username) => {
const response = await fetch(`https://jp-backend-kc80.onrender.com/api/getuser?username=${username}`);
const user = await response.json();
return user[0]; // Assuming the API returns an array
})
);
setFollowList(usersData.filter(user => user)); // Filter out any undefined users
} catch (error) {
console.error(`Error fetching ${type}:`, error);
setFollowList([]); // Reset on error
} finally {
setIsLoadingFollowList(false);
}
};
// Upload image and return URL
const uploadImage = async (file) => {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('https://jp-backend-kc80.onrender.com/upload/', {
method: 'POST',
body: formData,
});
const data = await response.json();
return data.image_url;
} catch (error) {
console.error('Error uploading image:', error);
return null;
}
};
const handleDeletePost = async (postId) => {
if (!window.confirm('Are you sure you want to delete this post?')) {
return;
}
try {
const response = await fetch(`https://jp-backend-kc80.onrender.com/api/userposts/${postId}/delete`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete post');
// Remove the post from local state
setUserData(prev => ({
...prev,
posts: prev.posts.filter(post => post.id !== postId),
postsCount: prev.postsCount - 1
}));
// Close the modal if open
setShowPostDetailModal(false);
alert('Post deleted successfully');
} catch (error) {
console.error('Error deleting post:', error);
alert('Failed to delete post. Please try again.');
}
};
// Handle new post submission
const handleNewPostSubmit = async (e) => {
e.preventDefault();
if (!newPost.image || !userData.username) return;
setIsUploading(true);
try {
const imageUrl = await uploadImage(newPost.image);
if (!imageUrl) throw new Error('Image upload failed');
const postData = {
username: userData.username,
image_url: imageUrl,
description: newPost.description,
tags: newPost.tags,
public: newPost.public
};
if (newPost.pin_id) {
postData.pin_id = newPost.pin_id;
}
const response = await fetch(`https://jp-backend-kc80.onrender.com/api/userposts?username=${userData.username}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
});
if (!response.ok) throw new Error('Post creation failed' + response + postData.pin_id);
await fetchUserPosts(userData.username);
setShowNewPostModal(false);
setNewPost({
image: null,
description: '',
tags: '',
public: false,
pin_id: null
});
} catch (error) {
console.error('Error creating post:', error);
alert('Failed to create post. Please try again.' + error);
} finally {
setIsUploading(false);
}
};
// Handle input changes
const handleInputChange = (e) => {
const { name, value, type, checked, files } = e.target;
setNewPost(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : (type === 'file' ? files[0] : value)
}));
};
// Handle edit profile changes
const handleEditProfileChange = (e) => {
const { name, value, files } = e.target;
setEditProfile(prev => ({
...prev,
[name]: name === 'profile_picture' ? files[0] : value
}));
};
// Handle edit profile submission
const handleEditProfileSubmit = async (e) => {
e.preventDefault();
if (!userData.username) return;
//setIsUploading(true);
try {
//let profilePicUrl = userData.profilePicture === defaultProfilePic ? '' : userData.profilePicture;
//if (editProfile.profile_picture) {
//profilePicUrl = await uploadImage(editProfile.profile_picture);
//if (!profilePicUrl) throw new Error('Profile picture upload failed');
//}
const formData = new FormData();
if (editProfile.bio) {
formData.append("bio", editProfile.bio); // profile_picture should be a File object
}
if (editProfile.profile_picture) {
formData.append("profile_picture", editProfile.profile_picture); // profile_picture should be a File object
}
const response = await fetch(`https://jp-backend-kc80.onrender.com/api/userupdate/${userData.username}`, {
method: 'PATCH',
body: formData,
});
if (!response.ok) throw new Error('Profile update failed' + response.status);
await fetchUserData(userData.username);
setShowEditProfileModal(false);
} catch (error) {
console.error('Error updating profile:', error);
alert('Failed to update profile. Please try again.' + error);
} finally {
setIsUploading(false);
}
};
const handleAddComment = async (e) => {
e.preventDefault();
if (!newComment.trim() || !selectedPost || !userData.username) return;
try {
const response = await fetch(`https://jp-backend-kc80.onrender.com/api/userposts/${selectedPost.id}/comment`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: userData.username,
comment: newComment
}),
});
if (!response.ok) throw new Error('Failed to add comment');
// Create the new comment entry
const newCommentEntry = {
[userData.username]: newComment
};
// Update the selected post
setSelectedPost(prev => ({
...prev,
comments: {
...(prev.comments || {}),
...newCommentEntry
}
}));
// Update the post in the main posts list
setUserData(prev => ({
...prev,
posts: prev.posts.map(post =>
post.id === selectedPost.id
? {
...post,
comments: {
...(post.comments || {}),
...newCommentEntry
}
}
: post
)
}));
setNewComment('');
} catch (error) {
console.error('Error adding comment:', error);
alert('Failed to add comment. Please try again.');
}
};
const handleLikePost = async (postId) => {
// Check if user already liked this post
if (likedPosts.includes(postId)) {
return;
}
try {
const response = await fetch(`https://jp-backend-kc80.onrender.com/api/userposts/${postId}/like`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) throw new Error('Failed to like post');
const result = await response.json();
// Update liked posts state
setLikedPosts(prev => [...prev, postId]);
// Update the selected post's like count
setSelectedPost(prev => ({
...prev,
likes: result.likes
}));
// Also update the post in the main posts list
setUserData(prev => ({
...prev,
posts: prev.posts.map(post =>
post.id === postId
? { ...post, likes: result.likes }
: post
)
}));
} catch (error) {
console.error('Error liking post:', error);
alert('Failed to like post. Please try again.');
}
};
// Open edit profile modal with current data
const openEditProfileModal = () => {
setEditProfile({
bio: userData.bio,
profile_picture: null
});
setShowEditProfileModal(true);
};
return (
<div>
<Navbar />
<div className="profile-container">
{/* Profile Header Section */}
<div className="profile-header">
<div className="profile-pic-container" style={{ visibility: isImageLoaded ? 'visible' : 'hidden' }}>
<img
src={userData.profilePicture || defaultProfilePic}
alt="Profile"
className="profile-picture"
onLoad={() => setIsImageLoaded(true)}
onError={(e) => {
e.target.onerror = null;
e.target.src = defaultProfilePic;
setIsImageLoaded(true);
}}
/>
</div>
<div className="profile-info">
<div className="username">{userData.username}</div>
<div className="stats">
<span><strong>{userData.postsCount}</strong> posts</span>
<span
className="clickable-stat"
onClick={() => showFollowList('followers')}
>
<strong>{userData.followers.length}</strong> followers
</span>
<span
className="clickable-stat"
onClick={() => showFollowList('following')}
>
<strong>{userData.following.length}</strong> following
</span>
</div>
<div className="bio">{userData.bio}</div>
<div className="profile-buttons">
<button className="edit-profile-btn" onClick={openEditProfileModal}>Edit Profile</button>
<button className="new-post-btn" onClick={() => setShowNewPostModal(true)}>New Post</button>
</div>
</div>
</div>
{/* Posts Grid Section */}
{userData.posts.length > 0 ? (
<div className="posts-grid">
{userData.posts.map((post, index) => (
<div key={index} className="post-item" onClick={() => handlePostClick(post)}>
<img src={post.image_url} alt={`Post ${index}`} />
<div className="post-overlay">
<span className="post-likes">❤️ {post.likes || 0}</span>
<span className="post-comments">💬 {Object.keys(post.comments || {}).length}</span>
</div>
</div>
))}
</div>
) : (
<div className="no-posts">
<h3>Share Photos</h3>
<p>When you share photos, they will appear on your profile.</p>
<button onClick={() => setShowNewPostModal(true)}>
Share your first photo
</button>
</div>
)}
</div>
{/* Post Detail Modal */}
{showPostDetailModal && selectedPost && (
<div className="popup-overlay" onClick={() => setShowPostDetailModal(false)}>
<motion.div
className="popup-content post-detail-modal"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={(e) => e.stopPropagation()}
>
<div className="post-detail-container">
<div className="post-image-container">
<img
src={selectedPost.image_url}
alt="Post"
className="post-detail-image"
/>
</div>
<div className="post-details">
<div className="post-user-info">
<div className="post-user-left">
<img
src={userData.profilePicture || defaultProfilePic}
alt="Profile"
className="post-user-avatar"
/>
<span className="post-username">{userData.username}</span>
</div>
{userData.username === selectedPost.username && (
<button
className="delete-post-btn"
onClick={() => handleDeletePost(selectedPost.id)}
>
Delete
</button>
)}
</div>
<div className="post-description">
<p>{selectedPost.description}</p>
</div>
<div className="post-tags">
<strong>Tags:</strong> {selectedPost.tags || 'No tags'}
</div>
{selectedPost.pin_id && (
<div className="post-pin-info">
<strong>Location:</strong>
{pins.find(pin => pin.id === selectedPost.pin_id)?.pin_name || 'Unknown location'}
<button
className="go-to-pin-btn"
onClick={() => {
const pin = pins.find(p => p.id === selectedPost.pin_id);
if (pin) {
navigate('/visit', {
state: {
pinLocation: { lat: pin.y, lng: pin.x }
}
});
}
}}
>
Go to Pin
</button>
</div>
)}
<div className="post-stats">
<button
className={`like-button ${likedPosts.includes(selectedPost.id) ? 'liked' : ''}`}
onClick={() => handleLikePost(selectedPost.id)}
disabled={likedPosts.includes(selectedPost.id)}
>
{likedPosts.includes(selectedPost.id) ? (
'❤️ Liked'
) : (
`❤️ ${selectedPost.likes || 0} likes`
)}
</button>
<span>💬 {Object.keys(selectedPost.comments || {}).length} comments</span>
</div>
{/* Comments section */}
<div className="post-comments-section">
<h4>Comments</h4>
{Object.keys(selectedPost.comments || {}).length > 0 ? (
<div className="comments-list">
{Object.entries(selectedPost.comments || {}).map(([username, comment]) => (
<div key={username} className="comment">
<strong>{username}:</strong> {comment}
</div>
))}
</div>
) : (
<p>No comments yet</p>
)}
</div>
<div className="add-comment">
<form onSubmit={handleAddComment}>
<input
type="text"
placeholder="Add a comment..."
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
/>
<button type="submit">Post</button>
</form>
</div>
<div className="post-date">
Posted on: {new Date(selectedPost.created_at).toLocaleDateString()}
</div>
</div>
</div>
</motion.div>
</div>
)}
{/* New Post Modal */}
{showNewPostModal && (
<div className="popup-overlay" onClick={() => !isUploading && setShowNewPostModal(false)}>
<motion.div
className="popup-content with-image-preview"
initial={{ y: -50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -50, opacity: 0 }}
transition={{ duration: 0.3 }}
onClick={(e) => e.stopPropagation()}
>
<div className="image-preview-container">
{newPost.image ? (
<img
src={URL.createObjectURL(newPost.image)}
alt="Preview"
className="preview-image"
/>
) : (
<div className="image-upload-prompt">
<p>Image preview will appear here</p>
</div>
)}
</div>
<div className="form-container">
<h2>Create Post</h2>
<form onSubmit={handleNewPostSubmit}>
<div className="form-group">
<label>Upload Image</label>
<input
type="file"
accept="image/*"
name="image"
onChange={handleInputChange}
required
disabled={isUploading}
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
name="description"
value={newPost.description}
onChange={handleInputChange}
disabled={isUploading}
/>
</div>
<div className="form-group">
<label>Tags (comma separated)</label>
<input
type="text"
name="tags"
value={newPost.tags}
onChange={handleInputChange}
disabled={isUploading}
/>
</div>
<div className="form-group">
<label>Associate with Pin (required)</label>
<select
name="pin_id"
value={newPost.pin_id || ''}
onChange={handleInputChange}
disabled={isUploading}
>
<option value="">No pin selected</option>
{pins.length > 0 ? (
pins.map((pin) => (
<option key={pin.id} value={pin.id}>
{pin.pin_name} ({pin.x.toFixed(2)}, {pin.y.toFixed(2)})
</option>
))
) : (
<option value="" disabled>
No pins available - Create your first pin
</option>
)}
</select>
{pins.length === 0 && (
<button
className="create-pin-button"
onClick={() => {
setShowNewPostModal(false);
navigate('/visit'); // Or whatever your pin creation route is
}}
>
Create Pin
</button>
)}
</div>
<div className="form-group checkbox">
<label>
<input
type="checkbox"
name="public"
checked={newPost.public}
onChange={handleInputChange}
disabled={isUploading}
/>
Public Post
</label>
</div>
<div className="form-actions">
<button
type="button"
onClick={() => !isUploading && setShowNewPostModal(false)}
disabled={isUploading}
>
Cancel
</button>
<button type="submit" disabled={isUploading}>
{isUploading ? 'Posting...' : 'Post'}
</button>
</div>
</form>
</div>
</motion.div>
</div>
)}
{/* Edit Profile Modal */}
{showEditProfileModal && (
<div className="popup-overlay" onClick={() => !isUploading && setShowEditProfileModal(false)}>
<motion.div
className="popup-content with-image-preview"
initial={{ y: -50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -50, opacity: 0 }}
transition={{ duration: 0.3 }}
onClick={(e) => e.stopPropagation()}
>
<div className="image-preview-container">
{editProfile.profile_picture ? (
<img
src={URL.createObjectURL(editProfile.profile_picture)}
alt="Preview"
className="preview-image"
/>
) : (
<img
src={userData.profilePicture || defaultProfilePic}
alt="Current Profile"
className="current-profile-pic"
/>
)}
</div>
<div className="form-container">
<h2>Edit Profile</h2>
<form onSubmit={handleEditProfileSubmit}>
<div className="form-group">
<label>Profile Picture</label>
<input
type="file"
accept="image/*"
name="profile_picture"
onChange={handleEditProfileChange}
disabled={isUploading}
/>
</div>
<div className="form-group">
<label>Bio</label>
<textarea
name="bio"
value={editProfile.bio}
onChange={handleEditProfileChange}
disabled={isUploading}
/>
</div>
<div className="form-actions">
<button
type="button"
onClick={() => !isUploading && setShowEditProfileModal(false)}
disabled={isUploading}
>
Cancel
</button>
<button type="submit" disabled={isUploading}>
{isUploading ? 'Saving...' : 'Save Changes'}
</button>
</div>
</form>
</div>
</motion.div>
</div>
)}
{showFollowModal && (
<div className="popup-overlay" onClick={() => setShowFollowModal(false)}>
<motion.div
className="popup-content follow-list-modal"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={(e) => e.stopPropagation()}
>
<div className="follow-modal-header">
<h3>{followListType === 'followers' ? 'Followers' : 'Following'}</h3>
<button
className="close-modal-btn"
onClick={() => setShowFollowModal(false)}
>
×
</button>
</div>
{isLoadingFollowList ? (
<div className="loading">Loading...</div>
) : followList.length > 0 ? (
<div className="follow-list">
{followList.map((user) => (
<div
key={user.username}
className="follow-item"
onClick={() => {
setShowFollowModal(false);
navigate(`/profile/${user.username}`);
}}
>
<img
src={user.profile_picture || defaultProfilePic}
alt={user.username}
className="follow-avatar"
onError={(e) => {
e.target.onerror = null;
e.target.src = defaultProfilePic;
}}
/>
<span className="follow-username">{user.username}</span>
</div>
))}
</div>
) : (
<div className="empty-list">
No {followListType === 'followers' ? 'followers' : 'people followed'} yet
</div>
)}
</motion.div>
</div>
)}
{/* <Footer /> */}
</div>
);
};
export default ProfilePage;