JourneyPoint / journeypoint / frontend / src / Components / ProfilePage.js
ProfilePage.js
Raw
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;