JourneyPoint / journeypoint / frontend / src / Components / UserProfile.js
UserProfile.js
Raw
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { motion } from 'framer-motion';
import Navbar from './Navbar';
import Footer from './footer';
import './ProfilePage.css';

const UserProfile = () => {
  const navigate = useNavigate();
  const { username } = useParams();
  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 [loggedInUserData, setLoggedInUserData] = useState({
    username: '',
    following: []
  });
  const [showPostDetailModal, setShowPostDetailModal] = useState(false);
  const [selectedPost, setSelectedPost] = useState(null);
  const [likedPosts, setLikedPosts] = useState([]);
  const [isFollowing, setIsFollowing] = useState(false);
  const [isUpdatingFollow, setIsUpdatingFollow] = useState(false);
  const [pins, setPins] = useState([]);
  const [showFollowModal, setShowFollowModal] = useState(false);
  const [followListType, setFollowListType] = useState(''); // 'followers' or 'following'
  const [followList, setFollowList] = useState([]);
  const [isLoadingFollowList, setIsLoadingFollowList] = useState(false);

  // 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);
      setLoggedInUserData(prev => ({
        ...prev,
        username: storedUsername
      }));
      fetchLoggedInUserData(storedUsername);
      fetchUserData(username);
      fetchUserPosts(username);
      fetchUserPins(username);
    } else {
      navigate('/login');
    }
  }, [navigate, username]);

  useEffect(() => {
    if (loggedInUserData.username && userData.username) {
      fetchUserPosts(userData.username);
    }
  }, [isFollowing, loggedInUserData.username, userData.username]);

  // Check if logged in user is following this profile
  useEffect(() => {
    if (loggedInUserData.username && userData.followers) {
      setIsFollowing(userData.followers.includes(loggedInUserData.username));
    }
  }, [loggedInUserData.username, userData.followers]);

  // 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);
      }
    };

  // 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 logged in user data
  const fetchLoggedInUserData = 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) {
        setLoggedInUserData(prev => ({
          ...prev,
          following: user[0].following || []
        }));
      }
    } catch (error) {
      console.error('Error fetching logged in user data:', error);
    }
  };

  const fetchUserPosts = async (username) => {
    try {
      const response = await fetch(`https://jp-backend-kc80.onrender.com/api/userposts?username=${username}`);
      const data = await response.json();

      // Check if viewing own profile or is a follower
      const isOwnProfile = loggedInUserData.username === username;
      const isFollower = userData.followers.includes(loggedInUserData.username);

      // Filter posts based on visibility
      const filteredPosts = Array.isArray(data)
        ? data.filter(post => isOwnProfile || isFollower || post.public)
        : [];

      setUserData(prev => ({
        ...prev,
        posts: filteredPosts,
        postsCount: filteredPosts.length
      }));
    } catch (error) {
      console.error('Error fetching user posts:', error);
    }
  };

  const showFollowList = async (type) => {
    setFollowListType(type);
    setIsLoadingFollowList(true);
    setShowFollowModal(true);

    try {
      const response = await fetch(`https://jp-backend-kc80.onrender.com/api/getuser?username=${username}`);
      const user = await response.json();

      if (user.length > 0) {
        const usernames = type === 'followers' ? user[0].followers : user[0].following;

        // Fetch details for each user in the list
        const usersData = await Promise.all(
          usernames.map(async (username) => {
            const userResponse = await fetch(`https://jp-backend-kc80.onrender.com/api/getuser?username=${username}`);
            const user = await userResponse.json();
            return user[0];
          })
        );

        setFollowList(usersData.filter(user => user)); // Filter out any undefined users
      }
    } catch (error) {
      console.error('Error fetching follow list:', error);
    } finally {
      setIsLoadingFollowList(false);
    }
  };

  // Handle follow/unfollow action
  const handleFollow = async () => {
    if (!loggedInUserData.username || !username || isUpdatingFollow) return;

    setIsUpdatingFollow(true);
    try {
      if (isFollowing) {
        // Unfollow - remove logged in user from profile's followers and remove profile from logged in user's following
        const formData = new FormData();
        formData.append("unfollow_username", username);

        const response1 = await fetch(`https://jp-backend-kc80.onrender.com/api/userupdate/${loggedInUserData.username}/unfollow`, {
          method: 'PATCH',
          body: formData,
        });

        const result1 = await response1.json();
        if (!response1.ok) throw new Error(result1.message || 'Failed to remove follower');
        // Update local state based on successful responses
        setUserData(prev => ({
          ...prev,
          followers: prev.followers.filter(follower => follower !== loggedInUserData.username)
        }));

        setLoggedInUserData(prev => ({
          ...prev,
          following: prev.following.filter(following => following !== username)
        }));
      } else {
        // Follow - add logged in user to profile's followers and add profile to logged in user's following
        const formData = new FormData();
        formData.append("add_follower", loggedInUserData.username);

        const response1 = await fetch(`https://jp-backend-kc80.onrender.com/api/userupdate/${userData.username}`, {
          method: 'PATCH',
          body: formData,
        });

        const result1 = await response1.json();
        if (!response1.ok) throw new Error(result1.message || 'Failed to add follower' + loggedInUserData.username + userData.username + result1.add_follower);

        const formData2 = new FormData();
        formData2.append("add_following", username);

        const response2 = await fetch(`https://jp-backend-kc80.onrender.com/api/userupdate/${loggedInUserData.username}`, {
          method: 'PATCH',
          body: formData2,
        });

        const result2 = await response2.json();
        if (!response2.ok) throw new Error(result2.message || 'Failed to add following');

        // Update local state based on successful responses
        setUserData(prev => ({
          ...prev,
          followers: [...prev.followers, loggedInUserData.username]
        }));

        setLoggedInUserData(prev => ({
          ...prev,
          following: [...prev.following, username]
        }));
      }

      // Toggle follow state after successful updates
      setIsFollowing(!isFollowing);
    } catch (error) {
      console.error('Error updating follow status:', error);
      alert(error.message || 'Failed to update follow status. Please try again.');
    } finally {
      setIsUpdatingFollow(false);
    }
  };

  const handleAddComment = async (e) => {
    e.preventDefault();
    if (!newComment.trim() || !selectedPost || !loggedInUserData.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: loggedInUserData.username,
          comment: newComment
        }),
      });

      if (!response.ok) throw new Error('Failed to add comment');

      // Create the new comment entry
      const newCommentEntry = {
        [loggedInUserData.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.');
    }
  };

  const canViewPost = (post) => {
    return (
      loggedInUserData.username === userData.username || // Own post
      isFollowing || // Is a follower
      post.public // Public post
    );
  };

  // Then update your handlePostClick:
  const handlePostClick = (post) => {
    if (!canViewPost(post)) {
      alert("You must follow this user to view their private posts");
      return;
    }
    setSelectedPost(post);
    setShowPostDetailModal(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">
              {loggedInUserData.username !== username && (
                  <button
                    className={`follow-btn ${isFollowing ? 'following' : ''}`}
                    onClick={handleFollow}
                    disabled={isUpdatingFollow}
                  >
                    {isUpdatingFollow ? '...' : (isFollowing ? 'Following' : 'Follow')}
                  </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">
            {loggedInUserData.username === username ? (
                  <>
                    <h3>Share Photos</h3>
                    <p>When you share photos, they will appear on your profile.</p>
                  </>
                ) : isFollowing ? (
                  <p>{userData.username} hasn't posted anything yet.</p>
                ) : (
                  <p>{userData.username} only shares posts with followers.</p>
                )}
              </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">
                  <img
                    src={userData.profilePicture || defaultProfilePic}
                    alt="Profile"
                    className="post-user-avatar"
                  />
                  <span className="post-username">{userData.username}</span>
                </div>
                <div className="post-description">
                  <p>{selectedPost.description}</p>
                </div>
                <div className="post-tags">
                  <strong>Tags:</strong> {selectedPost.tags || 'No tags'}
                </div>
                 {/* Add this section for the pin location */}
                  {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>

                  {isLoggedIn && (
                    <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>
      )}

      {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' : 'following'} yet
              </div>
            )}
          </motion.div>
        </div>
      )}

      <Footer />
    </div>
  );
};

export default UserProfile;