Untitled

 avatar
user_1740036
plain_text
2 months ago
17 kB
4
Indexable
import React, { useState, useEffect } from "react";
import {
  collection,
  query,
  orderBy,
  onSnapshot,
  serverTimestamp,
  addDoc,
  doc,
  updateDoc,
  arrayUnion,
  arrayRemove,
  getDoc,
} from "firebase/firestore";
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { getAuth } from "firebase/auth";
import { db } from "../firebase";
import { motion, AnimatePresence } from "framer-motion";
import {
  Heart,
  MessageSquare,
  Plus,
  Loader2,
  Image,
  Lock,
  Globe,
  UserPlus,
  X,
} from "lucide-react";

// Reusable Alert Component
const Alert = ({ children, variant = "default", onClose }) => {
  const bgColor =
    variant === "destructive"
      ? "bg-red-50 border-red-200 text-red-600"
      : "bg-green-50 border-green-200 text-green-600";

  return (
    <div className={`p-4 rounded-lg border ${bgColor} relative`}>
      {children}
      {onClose && (
        <button
          onClick={onClose}
          className="absolute top-2 right-2 text-gray-500 hover:text-gray-700"
        >
          <X size={16} />
        </button>
      )}
    </div>
  );
};

// Helper: Format Firestore Timestamp
const formatTimestamp = (timestamp) => {
  if (!timestamp) return new Date().toLocaleString();
  return timestamp.toDate
    ? timestamp.toDate().toLocaleString()
    : new Date(timestamp).toLocaleString();
};

function SpacesFeed() {
  // STATE VARIABLES
  const [posts, setPosts] = useState([]);
  const [text, setText] = useState("");
  const [loading, setLoading] = useState(true);
  const [isPosting, setIsPosting] = useState(false);
  const [error, setError] = useState(null);

  // Image selection
  const [selectedImage, setSelectedImage] = useState(null);
  const [imagePreview, setImagePreview] = useState(null);

  // Privacy & notifications
  const [privacy, setPrivacy] = useState("private");
  const [notification, setNotification] = useState(null);

  // Comments
  const [activeCommentPostId, setActiveCommentPostId] = useState(null);
  const [commentText, setCommentText] = useState("");

  const currentUser = getAuth().currentUser;
  const storage = getStorage();
  const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB limit

  // ------------------ REAL-TIME FETCH POSTS ------------------
  useEffect(() => {
    const colRef = collection(db, "spaces");
    const q = query(colRef, orderBy("createdAt", "desc"));

    const unsubscribe = onSnapshot(
      q,
      (snapshot) => {
        try {
          const postsArr = snapshot.docs
            .map((docSnap) => {
              const data = docSnap.data();
              if (!data) return null;
              return {
                id: docSnap.id,
                text: data.text || "",
                userId: data.userId || "",
                userName: data.userName || "Anonymous",
                userAvatar: data.userAvatar || "/default-avatar.png",
                privacy: data.privacy || "private",
                imageUrl: data.imageUrl || null,
                createdAt: data.createdAt ? data.createdAt.toDate() : new Date(),
                likes: typeof data.likes === "number" ? data.likes : 0,
                likedBy: Array.isArray(data.likedBy) ? data.likedBy : [],
                comments: Array.isArray(data.comments) ? data.comments : [],
              };
            })
            .filter(Boolean);

          setPosts(postsArr);
          setLoading(false);
        } catch (err) {
          console.error("Data processing error:", err);
          setError("Failed to process post data");
          setLoading(false);
        }
      },
      (err) => {
        console.error("Snapshot error:", err);
        setError("Failed to load posts: " + err.message);
        setLoading(false);
      }
    );

    return () => unsubscribe();
  }, []);

  // ------------------ IMAGE SELECTION HANDLER ------------------
  const handleImageSelect = (e) => {
    const file = e.target.files[0];
    if (!file) return;
    if (file.size > MAX_FILE_SIZE) {
      alert("Image must be under 2MB!");
      return;
    }
    setSelectedImage(file);
    const reader = new FileReader();
    reader.onloadend = () => setImagePreview(reader.result);
    reader.readAsDataURL(file);
  };

  // ------------------ CREATE NEW POST ------------------
  const handleCreatePost = async () => {
    if (!currentUser) {
      setError("Please sign in to create a post!");
      return;
    }
    if (!text.trim() && !selectedImage) {
      setError("Post must contain text or an image!");
      return;
    }
    setIsPosting(true);
    setError(null);

    try {
      let imageUrl = null;
      if (selectedImage) {
        // Upload image to Storage
        const imageRef = ref(
          storage,
          `spaces/${currentUser.uid}/${Date.now()}_${selectedImage.name}`
        );
        await uploadBytes(imageRef, selectedImage);
        imageUrl = await getDownloadURL(imageRef);
      }

      const newPost = {
        userId: currentUser.uid,
        userName: currentUser.displayName || "Anonymous",
        userAvatar: currentUser.photoURL || "/default-avatar.png",
        text: text.trim(),
        imageUrl,
        createdAt: serverTimestamp(),
        privacy,
        likes: 0,
        likedBy: [],
        comments: [],
      };

      await addDoc(collection(db, "spaces"), newPost);

      // Reset the form
      setText("");
      setSelectedImage(null);
      setImagePreview(null);
      setPrivacy("private");

      // Show success notification
      setNotification({
        type: "success",
        message: "Post created successfully!",
      });
      setTimeout(() => setNotification(null), 3000);
    } catch (err) {
      console.error("Post creation error:", err);
      setError("Failed to create post. Please try again.");
    } finally {
      setIsPosting(false);
    }
  };

  // ------------------ LIKE / UNLIKE HANDLER ------------------
  const handleToggleLike = async (post) => {
    if (!currentUser) {
      setError("Please sign in to like a post!");
      return;
    }
    try {
      const postRef = doc(db, "spaces", post.id);
      const alreadyLiked = post.likedBy.includes(currentUser.uid);

      // If user has already liked, we remove them; else we add them
      const newLikes = alreadyLiked
        ? post.likes - 1
        : post.likes + 1;

      await updateDoc(postRef, {
        likes: newLikes,
        likedBy: alreadyLiked
          ? arrayRemove(currentUser.uid)
          : arrayUnion(currentUser.uid),
      });
    } catch (err) {
      console.error("Error toggling like:", err);
      setError("Failed to update like. Please try again.");
    }
  };

  // ------------------ TOGGLE COMMENT SECTION ------------------
  const handleToggleComments = (postId) => {
    setActiveCommentPostId((prev) => (prev === postId ? null : postId));
    setCommentText("");
  };

  // ------------------ ADD COMMENT ------------------
  const handleAddComment = async (postId) => {
    if (!currentUser) {
      setError("Please sign in to comment!");
      return;
    }
    if (!commentText.trim()) {
      setError("Comment cannot be empty!");
      return;
    }
    const postRef = doc(db, "spaces", postId);

    const newComment = {
      userId: currentUser.uid,
      userName: currentUser.displayName || "Anonymous",
      text: commentText.trim(),
      createdAt: serverTimestamp(),
    };

    try {
      await updateDoc(postRef, {
        comments: arrayUnion(newComment),
      });
      setCommentText("");
      setError(null);
    } catch (err) {
      console.error("Failed to add comment:", err);
      setError("Failed to add comment. Please try again.");
    }
  };

  // ------------------ RENDER ------------------
  if (loading) {
    return (
      <div className="flex items-center justify-center min-h-screen bg-pink-50">
        <Loader2 className="w-8 h-8 animate-spin text-pink-500" />
      </div>
    );
  }

  return (
    <div className="min-h-screen p-4 bg-gradient-to-br from-pink-50 to-white">
      <div className="max-w-lg mx-auto">
        <h1 className="text-2xl font-bold mb-6 text-gray-800">My Spaces</h1>

        {/* Notification */}
        {notification && (
          <div className="mb-4">
            <Alert onClose={() => setNotification(null)}>
              {notification.message}
            </Alert>
          </div>
        )}

        {/* Error Alert */}
        {error && (
          <div className="mb-4">
            <Alert variant="destructive" onClose={() => setError(null)}>
              {error}
            </Alert>
          </div>
        )}

        {/* CREATE POST FORM */}
        <div className="mb-6 p-4 bg-white rounded-lg shadow-lg">
          <textarea
            value={text}
            onChange={(e) => setText(e.target.value)}
            rows="3"
            placeholder="Share your thoughts, experiences, or stories..."
            className="w-full p-3 border border-gray-200 rounded-lg 
                       focus:outline-none focus:ring-2 focus:ring-pink-200 
                       transition-all"
            disabled={isPosting}
          />

          {/* IMAGE PREVIEW */}
          {imagePreview && (
            <div className="relative mt-2">
              <img
                src={imagePreview}
                alt="Preview"
                className="max-h-48 rounded-lg object-cover"
              />
              <button
                onClick={() => {
                  setSelectedImage(null);
                  setImagePreview(null);
                }}
                className="absolute top-2 right-2 p-1 bg-gray-800 bg-opacity-50 
                           rounded-full text-white hover:bg-opacity-70"
              >
                ×
              </button>
            </div>
          )}

          <div className="flex justify-between items-center mt-3">
            <div className="flex items-center gap-4">
              {/* Image Input */}
              <label className="cursor-pointer text-pink-500 hover:text-pink-600">
                <Image size={20} />
                <input
                  type="file"
                  accept="image/*"
                  className="hidden"
                  onChange={handleImageSelect}
                  disabled={isPosting}
                />
              </label>

              {/* Privacy Selection */}
              <select
                value={privacy}
                onChange={(e) => setPrivacy(e.target.value)}
                className="text-sm border rounded-lg px-2 py-1"
              >
                <option value="private">Private</option>
                <option value="friends">Friends</option>
                <option value="public">Public</option>
              </select>
            </div>

            {/* Post Button */}
            <button
              onClick={handleCreatePost}
              disabled={isPosting || (!text.trim() && !selectedImage)}
              className="px-4 py-2 bg-pink-500 text-white rounded-lg 
                         hover:bg-pink-600 transition-all flex items-center gap-2 
                         disabled:opacity-50"
            >
              {isPosting ? (
                <Loader2 className="w-4 h-4 animate-spin" />
              ) : (
                <Plus size={16} />
              )}
              <span>{isPosting ? "Posting..." : "Post"}</span>
            </button>
          </div>
        </div>

        {/* POSTS LIST */}
        <AnimatePresence>
          {posts.map((post) => (
            <motion.div
              key={post.id}
              className="mb-4 bg-white rounded-lg shadow-lg p-4"
              initial={{ opacity: 0, y: 20 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: -20 }}
            >
              {/* Post Header */}
              <div className="flex items-center justify-between mb-3">
                <div className="flex items-center gap-3">
                  <img
                    src={post.userAvatar}
                    alt={`${post.userName}'s avatar`}
                    className="w-10 h-10 rounded-full object-cover border-2 border-pink-100"
                  />
                  <div>
                    <p className="font-semibold text-gray-800">{post.userName}</p>
                    <div className="flex items-center gap-1 text-xs text-gray-500">
                      <span>{formatTimestamp(post.createdAt)}</span>
                      {post.privacy === "private" && <Lock size={12} />}
                      {post.privacy === "friends" && <UserPlus size={12} />}
                      {post.privacy === "public" && <Globe size={12} />}
                    </div>
                  </div>
                </div>
              </div>

              {/* Post Text */}
              <p className="text-gray-700 mb-4 whitespace-pre-wrap">
                {post.text}
              </p>

              {/* Post Image */}
              {post.imageUrl && (
                <img
                  src={post.imageUrl}
                  alt="Post content"
                  className="mb-4 rounded-lg max-h-96 w-full object-cover"
                />
              )}

              {/* Like / Comment Buttons */}
              <div className="flex items-center gap-6 text-sm text-gray-500">
                <button
                  className="flex items-center gap-1 hover:text-pink-500 transition-colors"
                  onClick={() => handleToggleLike(post)}
                >
                  <Heart
                    size={18}
                    className={
                      post.likedBy?.includes(currentUser?.uid)
                        ? "fill-pink-500 text-pink-500"
                        : ""
                    }
                  />
                  <span>{post.likes}</span>
                </button>

                <button
                  className="flex items-center gap-1 hover:text-pink-500 transition-colors"
                  onClick={() => handleToggleComments(post.id)}
                >
                  <MessageSquare size={18} />
                  <span>{post.comments?.length}</span>
                </button>
              </div>

              {/* Comments Section */}
              {activeCommentPostId === post.id && (
                <div className="mt-3 bg-pink-50 rounded-lg p-3">
                  {post.comments?.length > 0 ? (
                    post.comments.map((c, idx) => {
                      const commentDate = formatTimestamp(c.createdAt);
                      return (
                        <div
                          key={idx}
                          className="mb-2 bg-white p-2 rounded shadow-sm"
                        >
                          <p className="text-sm font-semibold text-gray-700">
                            {c.userName}{" "}
                            <span className="text-xs text-gray-400 ml-2">
                              {commentDate}
                            </span>
                          </p>
                          <p className="text-sm text-gray-600">{c.text}</p>
                        </div>
                      );
                    })
                  ) : (
                    <p className="text-gray-500 text-sm">
                      No comments yet. Be the first to comment!
                    </p>
                  )}
                  <div className="flex items-center gap-2 mt-2">
                    <input
                      type="text"
                      value={commentText}
                      onChange={(e) => setCommentText(e.target.value)}
                      placeholder="Write a comment..."
                      className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded"
                    />
                    <button
                      onClick={() => handleAddComment(post.id)}
                      className="px-3 py-2 bg-pink-500 text-white rounded 
                                 hover:bg-pink-600 transition-colors text-sm"
                    >
                      Post
                    </button>
                  </div>
                </div>
              )}
            </motion.div>
          ))}
        </AnimatePresence>

        {/* Fallback when no posts exist */}
        {!loading && posts.length === 0 && (
          <div className="text-center py-10">
            <p className="text-gray-500">
              No posts yet. Be the first to share something!
            </p>
          </div>
        )}
      </div>
    </div>
  );
}

export default SpacesFeed;
Editor is loading...
Leave a Comment