From 1538cbbf32d2dce011343ec7885190fb9ddd91db Mon Sep 17 00:00:00 2001 From: Mahesh N Date: Sun, 28 Jun 2026 01:44:45 +0530 Subject: [PATCH 1/2] feat: mark as solved implemented --- app/dashboard/page.js | 902 +++++++++++++++++++++++++++++ components/Notifications.js | 307 +++++++++- components/dashboard/FeedColumn.js | 255 +++++++- components/dashboard/PostCard.js | 834 +++++++++++++++++++++++++- 4 files changed, 2293 insertions(+), 5 deletions(-) diff --git a/app/dashboard/page.js b/app/dashboard/page.js index 48090c1..33c59ff 100644 --- a/app/dashboard/page.js +++ b/app/dashboard/page.js @@ -735,6 +735,29 @@ export default function Dashboard() { } }; + // ── Mark as Solved ──────────────────────────────────────────────────────── + const handleMarkSolved = async (post, comment) => { + if (!user || user.uid !== post.uid) return; + const alreadySolved = post.solved && post.solvedCommentId === comment.createdAt; + try { + await updateDoc(doc(db, "posts", post.id), { + solved: !alreadySolved, + solvedCommentId: alreadySolved ? null : comment.createdAt, + }); + if (!alreadySolved && comment.uid !== user.uid) { + await createNotification({ + toUid: comment.uid, + fromUser: user, + type: "solved", + postId: post.id, + }); + } + } catch (err) { + console.error(err); + setError("Failed to update solved status."); + } + }; + // ── Shared props bundles ────────────────────────────────────────────────── const sharedPostProps = { user, @@ -766,6 +789,7 @@ export default function Dashboard() { onDeleteComment: handleDeleteComment, onVotePoll: handleVotePoll, onToggleCommentReaction: handleToggleCommentReaction, + onMarkSolved: handleMarkSolved, }; const composerProps = { @@ -875,3 +899,881 @@ export default function Dashboard() { ); } + +// "use client"; + +// import { +// addDoc, +// arrayRemove, +// arrayUnion, +// collection, +// deleteDoc, +// doc, +// getDoc, +// onSnapshot, +// orderBy, +// query, +// serverTimestamp, +// setDoc, +// updateDoc, +// where, +// } from "firebase/firestore"; +// import { useRouter } from "next/navigation"; +// import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +// import CodeEditorModal from "../../components/CodeEditorModal"; +// import FeedColumn from "../../components/dashboard/FeedColumn"; +// import LeftSidebar from "../../components/dashboard/LeftSidebar"; +// import MobileView from "../../components/dashboard/MobileView"; +// import ProfilePopup from "../../components/dashboard/ProfilePopup"; +// import RightSidebar from "../../components/dashboard/RightSidebar"; +// import FeatureTour from "../../components/FeatureTour"; +// import Navbar from "../../components/Navbar"; +// import ProtectedRoute from "../../components/ProtectedRoute"; +// import SavedPosts from "../../components/SavedPosts"; +// import { useAuth } from "../../context/AuthContext"; +// import { db } from "../../lib/firebase"; +// import { createNotification } from "../../lib/notifications"; +// import { EVENTS } from "../../lib/posthog/events"; +// import { captureEvent } from "../../lib/posthog/helpers"; + +// const FEATURE_TOUR_KEY = "devconnect_feature_tour_seen"; + +// const S = { +// appContainer: { +// display: "flex", +// flexDirection: "column", +// minHeight: "100vh", +// backgroundColor: "var(--bg-primary)", +// }, +// mainLayout: { +// display: "grid", +// gridTemplateColumns: "240px 1fr 340px", +// gap: 24, +// maxWidth: 1440, +// width: "100%", +// margin: "0 auto", +// padding: "24px 24px 24px", +// flex: 1, +// }, +// }; + +// // ── Streak helper ───────────────────────────────────────────────────────────── +// async function updateStreak(uid) { +// const userRef = doc(db, "users", uid); +// const snap = await getDoc(userRef); +// const data = snap.exists() ? snap.data() : {}; + +// const todayStr = new Date().toISOString().slice(0, 10); +// const lastActiveStr = data.lastActiveDate || ""; +// const currentStreak = data.streak || 0; + +// if (lastActiveStr === todayStr) return; + +// const yesterday = new Date(); +// yesterday.setDate(yesterday.getDate() - 1); +// const yesterdayStr = yesterday.toISOString().slice(0, 10); + +// const newStreak = lastActiveStr === yesterdayStr ? currentStreak + 1 : 1; + +// await setDoc( +// userRef, +// { streak: newStreak, lastActiveDate: todayStr }, +// { merge: true }, +// ); +// } + +// export default function Dashboard() { +// const { user } = useAuth(); +// const router = useRouter(); + +// // ── Composer state ─────────────────────────────────────────────────────── +// const [content, setContent] = useState(""); +// const [postType, setPostType] = useState("discussion"); +// const [selectedTags, setSelectedTags] = useState([]); +// const [customTag, setCustomTag] = useState(""); +// const [showAiDraft, setShowAiDraft] = useState(false); +// const [showCodeEditor, setShowCodeEditor] = useState(false); + +// const [error, setError] = useState(""); +// const [pollOptions, setPollOptions] = useState(["", ""]); +// const [imageURL, setImageURL] = useState(""); +// const [imageUploading, setImageUploading] = useState(false); + +// // ── Feed / UI state ────────────────────────────────────────────────────── +// const [posts, setPosts] = useState([]); +// const [activeTab, setActiveTab] = useState("latest"); +// const [activeTag, setActiveTag] = useState(null); +// const [activeMembers, setActiveMembers] = useState([]); +// const [usersCache, setUsersCache] = useState({}); +// const [highlightedPostId, setHighlightedPostId] = useState(null); +// const [profilePopup, setProfilePopup] = useState(null); +// const [following, setFollowing] = useState([]); +// const [savedPostIds, setSavedPostIds] = useState([]); +// const [showSavedPosts, setShowSavedPosts] = useState(false); +// const [showFeatureTour, setShowFeatureTour] = useState(false); +// const [streak, setStreak] = useState(0); + +// // ── Post editing state ─────────────────────────────────────────────────── +// const [editingId, setEditingId] = useState(null); +// const [editContent, setEditContent] = useState(""); + +// // ── Comment state ──────────────────────────────────────────────────────── +// const [openCommentsFor, setOpenCommentsFor] = useState(null); +// const [commentDraft, setCommentDraft] = useState(""); +// const [editingComment, setEditingComment] = useState(null); +// const [editingCommentDraft, setEditingCommentDraft] = useState(""); + +// // ── Mobile state ───────────────────────────────────────────────────────── +// const [isMobile, setIsMobile] = useState(false); +// const [mobileTab, setMobileTab] = useState("feed"); + +// const highlightTimeoutRef = useRef(null); +// const isMobileRef = useRef(false); +// const scrolledRef = useRef(false); + +// // ── Responsive detection ───────────────────────────────────────────────── +// useEffect(() => { +// const check = () => { +// const mobile = window.innerWidth < 1024; +// setIsMobile(mobile); +// isMobileRef.current = mobile; +// }; +// check(); +// window.addEventListener("resize", check); +// return () => window.removeEventListener("resize", check); +// }, []); + +// // ── Feature tour ───────────────────────────────────────────────────────── +// useEffect(() => { +// try { +// const seen = localStorage.getItem(FEATURE_TOUR_KEY); +// if (!seen) setShowFeatureTour(true); +// } catch {} +// }, []); + +// const closeFeatureTour = () => { +// setShowFeatureTour(false); +// try { +// localStorage.setItem(FEATURE_TOUR_KEY, "true"); +// } catch {} +// }; + +// // ── Firebase: posts ─────────────────────────────────────────────────────── +// useEffect(() => { +// const postsQuery = query( +// collection(db, "posts"), +// orderBy("timestamp", "desc"), +// ); +// const unsubscribe = onSnapshot( +// postsQuery, +// (snapshot) => { +// setPosts(snapshot.docs.map((d) => ({ id: d.id, ...d.data() }))); +// }, +// (err) => { +// console.error(err); +// setError("Failed to load posts."); +// }, +// ); +// return () => unsubscribe(); +// }, []); + +// // ── Firebase: users cache ───────────────────────────────────────────────── +// useEffect(() => { +// const uids = new Set(); +// posts.forEach((p) => { +// if (p.uid) uids.add(p.uid); +// (p.comments || []).forEach((c) => { +// if (c.uid) uids.add(c.uid); +// }); +// }); +// const unsubs = []; +// uids.forEach((uid) => { +// const unsub = onSnapshot(doc(db, "users", uid), (snap) => { +// if (snap.exists()) { +// const data = snap.data(); +// setUsersCache((prev) => ({ +// ...prev, +// [uid]: { +// photoURL: data.photoURL || "", +// displayName: data.displayName || "", +// followersCount: (data.followers || []).length, +// followingCount: (data.following || []).length, +// }, +// })); +// } +// }); +// unsubs.push(unsub); +// }); +// return () => unsubs.forEach((u) => u()); +// }, [posts]); + +// // ── Firebase: current user's saved posts + following + streak ───────────── +// useEffect(() => { +// if (!user) { +// setSavedPostIds([]); +// setFollowing([]); +// setStreak(0); +// return; +// } +// const unsubscribe = onSnapshot( +// doc(db, "users", user.uid), +// (snap) => { +// const data = snap.data() || {}; +// setSavedPostIds(data.savedPosts || []); +// setFollowing(data.following || []); +// setStreak(data.streak || 0); +// }, +// (err) => console.error(err), +// ); +// return () => unsubscribe(); +// }, [user]); + +// // ── Firebase: active members ────────────────────────────────────────────── +// useEffect(() => { +// const membersQuery = query( +// collection(db, "users"), +// where("isOnline", "==", true), +// ); +// const unsubscribe = onSnapshot( +// membersQuery, +// (snapshot) => { +// setActiveMembers(snapshot.docs.map((d) => ({ id: d.id, ...d.data() }))); +// }, +// (err) => console.error(err), +// ); +// return () => unsubscribe(); +// }, []); + +// // ── Close popup on scroll ───────────────────────────────────────────────── +// useEffect(() => { +// if (!profilePopup) return; +// const handleScroll = () => setProfilePopup(null); +// window.addEventListener("scroll", handleScroll, { passive: true }); +// return () => window.removeEventListener("scroll", handleScroll); +// }, [profilePopup]); + +// // ── Deep-link scroll to post via hash ──────────────────────────────────── +// const scrollToHashPost = useCallback(() => { +// const hash = window.location.hash; +// if (!hash || !hash.startsWith("#post-")) return; +// const targetId = hash.replace("#post-", ""); +// if (!posts.some((p) => p.id === targetId)) return; +// setMobileTab("feed"); +// setActiveTab("latest"); +// setTimeout(() => { +// const el = document.getElementById(`post-${targetId}`); +// if (el) { +// el.scrollIntoView({ behavior: "smooth", block: "center" }); +// setHighlightedPostId(targetId); +// if (highlightTimeoutRef.current) +// clearTimeout(highlightTimeoutRef.current); +// highlightTimeoutRef.current = setTimeout( +// () => setHighlightedPostId(null), +// 2600, +// ); +// } +// }, 150); +// }, [posts]); + +// useEffect(() => { +// if (scrolledRef.current) return; +// const hash = window.location.hash; +// if (!hash || !hash.startsWith("#post-")) return; +// const targetId = hash.replace("#post-", ""); +// if (!posts.some((p) => p.id === targetId)) return; +// scrolledRef.current = true; +// scrollToHashPost(); +// }, [posts, scrollToHashPost]); + +// useEffect(() => { +// const handler = () => { +// scrolledRef.current = false; +// scrollToHashPost(); +// }; +// window.addEventListener("hashchange", handler); +// window.addEventListener("dashboard-scroll-request", handler); +// return () => { +// window.removeEventListener("hashchange", handler); +// window.removeEventListener("dashboard-scroll-request", handler); +// }; +// }, [scrollToHashPost]); + +// useEffect(() => { +// return () => { +// if (highlightTimeoutRef.current) +// clearTimeout(highlightTimeoutRef.current); +// }; +// }, []); + +// // ── Live name / photo helpers ───────────────────────────────────────────── +// const getLiveName = useCallback( +// (uid, fallback) => +// usersCache[uid]?.displayName || fallback || "Anonymous User", +// [usersCache], +// ); +// const getLivePhoto = useCallback( +// (uid, fallback) => usersCache[uid]?.photoURL ?? fallback ?? "", +// [usersCache], +// ); + +// // ── Profile popup ───────────────────────────────────────────────────────── +// const handleViewProfile = useCallback( +// (uid) => { +// router.push(`/user/${uid}`); +// }, +// [router], +// ); + +// const openProfile = useCallback( +// (e, uid, storedName, storedPhoto) => { +// e.stopPropagation(); +// if (isMobileRef.current) { +// router.push(`/user/${uid}`); +// return; +// } +// const rect = e.currentTarget.getBoundingClientRect(); +// const popupWidth = 224, +// popupHeight = 300, +// margin = 12; +// let x = rect.right + margin, +// y = rect.top + rect.height / 2; +// if (x + popupWidth > window.innerWidth - 16) +// x = rect.left - popupWidth - margin; +// const halfPopup = popupHeight / 2; +// if (y - halfPopup < 8) y = halfPopup + 8; +// if (y + halfPopup > window.innerHeight - 8) +// y = window.innerHeight - halfPopup - 8; +// setProfilePopup({ +// uid, +// displayName: +// usersCache[uid]?.displayName || storedName || "Anonymous User", +// photoURL: usersCache[uid]?.photoURL ?? storedPhoto ?? "", +// followersCount: usersCache[uid]?.followersCount ?? 0, +// followingCount: usersCache[uid]?.followingCount ?? 0, +// x, +// y, +// flipped: rect.right + margin + popupWidth > window.innerWidth - 16, +// }); +// }, +// [usersCache, router], +// ); + +// // ── Follow / Unfollow ───────────────────────────────────────────────────── +// // ── Comment reactions ──────────────────────────────────────────────────── +// // ── Comment reactions (one emoji per user per comment) ────────────────── +// const handleToggleCommentReaction = async (post, comment, emoji) => { +// if (!user) return; +// const reactions = comment.reactions || {}; + +// // Find which emoji (if any) this user currently has on this comment +// const currentEmoji = Object.keys(reactions).find((e) => +// (reactions[e] || []).includes(user.uid), +// ); + +// // Strip the user out of every emoji's reactor list first +// const updatedReactions = {}; +// Object.keys(reactions).forEach((e) => { +// const filtered = (reactions[e] || []).filter((uid) => uid !== user.uid); +// if (filtered.length > 0) updatedReactions[e] = filtered; +// }); + +// // If they clicked the emoji they already had, this is a toggle-off → leave removed. +// // Otherwise, add them to the newly clicked emoji. +// if (currentEmoji !== emoji) { +// updatedReactions[emoji] = [...(updatedReactions[emoji] || []), user.uid]; +// } + +// const updatedComments = (post.comments || []).map((c) => +// c.createdAt === comment.createdAt && c.uid === comment.uid +// ? { ...c, reactions: updatedReactions } +// : c, +// ); + +// try { +// await updateDoc(doc(db, "posts", post.id), { comments: updatedComments }); +// } catch (err) { +// console.error(err); +// setError("Failed to update reaction."); +// } +// }; +// const handleFollowToggle = useCallback( +// async (targetUid, isCurrentlyFollowing) => { +// if (!user || !targetUid || targetUid === user.uid) return; +// try { +// await setDoc( +// doc(db, "users", user.uid), +// { +// following: isCurrentlyFollowing +// ? arrayRemove(targetUid) +// : arrayUnion(targetUid), +// }, +// { merge: true }, +// ); +// await setDoc( +// doc(db, "users", targetUid), +// { +// followers: isCurrentlyFollowing +// ? arrayRemove(user.uid) +// : arrayUnion(user.uid), +// }, +// { merge: true }, +// ); +// if (!isCurrentlyFollowing) { +// await createNotification({ +// toUid: targetUid, +// fromUser: user, +// type: "follow", +// }); +// } +// } catch (err) { +// console.error("Follow toggle failed:", err); +// setError("Failed to update follow status."); +// } +// }, +// [user], +// ); + +// // ── Memoized derived feed data ──────────────────────────────────────────── +// const filteredPosts = useMemo(() => { +// let result = posts; +// if (activeTab === "questions") +// result = posts.filter((p) => p.postType === "question"); +// else if (activeTab === "collaboration") +// result = posts.filter((p) => p.postType === "collaboration"); +// if (activeTag) +// result = result.filter((p) => (p.tags || []).includes(activeTag)); +// return result; +// }, [posts, activeTab, activeTag]); + +// const trendingPosts = useMemo(() => { +// const cutoff = Date.now() - 48 * 60 * 60 * 1000; +// return posts +// .filter((p) => { +// const ts = p.timestamp?.toDate ? p.timestamp.toDate().getTime() : 0; +// return ts >= cutoff; +// }) +// .sort((a, b) => (b.likes || 0) - (a.likes || 0)) +// .slice(0, 10); +// }, [posts]); + +// const trendingTags = useMemo(() => { +// const counts = {}, +// newToday = {}; +// const startOfToday = new Date(); +// startOfToday.setHours(0, 0, 0, 0); +// posts.forEach((post) => { +// const postDate = post.timestamp?.toDate ? post.timestamp.toDate() : null; +// const isToday = postDate && postDate >= startOfToday; +// (post.tags || []).forEach((tag) => { +// counts[tag] = (counts[tag] || 0) + 1; +// if (isToday) newToday[tag] = (newToday[tag] || 0) + 1; +// }); +// }); +// return Object.entries(counts) +// .sort((a, b) => b[1] - a[1]) +// .slice(0, 5) +// .map(([tag, count]) => ({ +// tag, +// posts: `${count} post${count === 1 ? "" : "s"}`, +// new: newToday[tag] +// ? `+${newToday[tag]} new today` +// : "No new posts today", +// })); +// }, [posts]); + +// const handleCreatePost = async () => { +// if (!content.trim() || !user) return; +// // Poll validation +// if (postType === "poll") { +// const validOptions = pollOptions.filter((o) => o.trim()); +// if (validOptions.length < 2) { +// setError("Please add at least 2 poll options."); +// return; +// } +// } +// try { +// setError(""); +// const postData = { +// uid: user.uid, +// displayName: user.displayName || user.email || "Anonymous User", +// photoURL: user.photoURL || "", +// content: content.trim(), +// tags: selectedTags, +// postType, +// timestamp: serverTimestamp(), +// likes: 0, +// likedBy: [], +// comments: [], +// ...(imageURL ? { imageURL } : {}), +// }; +// if (postType === "poll") { +// postData.pollOptions = pollOptions.filter((o) => o.trim()); +// postData.pollVotes = {}; +// } +// await addDoc(collection(db, "posts"), postData); +// try { captureEvent(EVENTS.POST_CREATED, { postType, tagCount: selectedTags.length }); } catch (_) {} +// try { await updateStreak(user.uid); } catch (_) {} +// // reset all composer fields on success +// setContent(""); +// setSelectedTags([]); +// setCustomTag(""); +// setPostType("discussion"); +// setPollOptions(["", ""]); +// setImageURL(""); +// setImageUploading(false); +// setError(""); +// } catch (err) { +// console.error(err); +// setError("Failed to create post. Please try again."); +// throw err; // re-throw so PostComposer's local posting state can reset +// } +// }; + +// const handleInsertCode = (codeBlock) => { +// setContent((prev) => prev + (prev ? "\n\n" : "") + codeBlock); +// setShowCodeEditor(false); +// }; + +// const handleToggleLike = async (post) => { +// if (!user) return; +// const liked = (post.likedBy || []).includes(user.uid); +// try { +// await updateDoc(doc(db, "posts", post.id), { +// likedBy: liked ? arrayRemove(user.uid) : arrayUnion(user.uid), +// likes: (post.likedBy || []).length + (liked ? -1 : 1), +// }); +// if (!liked) { +// await createNotification({ +// toUid: post.uid, +// fromUser: user, +// type: "like", +// postId: post.id, +// }); +// } +// } catch (err) { +// console.error(err); +// setError("Failed to update like."); +// } +// }; + +// const handleToggleSave = async (postId) => { +// if (!user) return; +// const isSaved = savedPostIds.includes(postId); +// try { +// await setDoc( +// doc(db, "users", user.uid), +// { +// savedPosts: isSaved ? arrayRemove(postId) : arrayUnion(postId), +// }, +// { merge: true }, +// ); + +// const post = posts.find((p) => p.id === postId); +// const currentCount = post?.saveCount || 0; +// await updateDoc(doc(db, "posts", postId), { +// saveCount: isSaved ? Math.max(0, currentCount - 1) : currentCount + 1, +// }); +// } catch (err) { +// console.error(err); +// setError("Failed to update saved posts."); +// } +// }; + +// const handleDeletePost = async (postId) => { +// if (!window.confirm("Delete this post? This cannot be undone.")) return; +// try { +// await deleteDoc(doc(db, "posts", postId)); +// } catch (err) { +// console.error(err); +// setError("Failed to delete post."); +// } +// }; + +// const startEdit = (post) => { +// setEditingId(post.id); +// setEditContent(post.content); +// }; +// const cancelEdit = () => { +// setEditingId(null); +// setEditContent(""); +// }; +// const handleSaveEdit = async (postId) => { +// if (!editContent.trim()) return; +// try { +// await updateDoc(doc(db, "posts", postId), { +// content: editContent.trim(), +// edited: true, +// }); +// setEditingId(null); +// setEditContent(""); +// } catch (err) { +// console.error(err); +// setError("Failed to update post."); +// } +// }; + +// // ── Poll voting ─────────────────────────────────────────────────────────── +// const handleVotePoll = async (post, optionIdx) => { +// if (!user) return; +// const votes = post.pollVotes || {}; +// const options = post.pollOptions || []; + +// // Find if user already voted for any option +// const prevVotedIdx = options.findIndex((_, idx) => +// (votes[idx] || []).includes(user.uid), +// ); + +// // Build updated votes object +// const updatedVotes = { ...votes }; + +// // Remove previous vote if any +// if (prevVotedIdx !== -1 && prevVotedIdx !== optionIdx) { +// updatedVotes[prevVotedIdx] = (votes[prevVotedIdx] || []).filter( +// (uid) => uid !== user.uid, +// ); +// } + +// // Toggle: if clicking same option, remove vote; otherwise add +// if (prevVotedIdx === optionIdx) { +// updatedVotes[optionIdx] = (votes[optionIdx] || []).filter( +// (uid) => uid !== user.uid, +// ); +// } else { +// updatedVotes[optionIdx] = [...(votes[optionIdx] || []), user.uid]; +// } + +// try { +// await updateDoc(doc(db, "posts", post.id), { pollVotes: updatedVotes }); +// } catch (err) { +// console.error(err); +// setError("Failed to record vote."); +// } +// }; + +// // ── Comment CRUD ────────────────────────────────────────────────────────── +// const toggleComments = (postId) => { +// setOpenCommentsFor((prev) => (prev === postId ? null : postId)); +// setCommentDraft(""); +// setEditingComment(null); +// }; + +// const handleAddComment = async (post) => { +// if (!commentDraft.trim() || !user) return; +// const trimmed = commentDraft.trim(); +// const newComment = { +// uid: user.uid, +// displayName: user.displayName || user.email || "Anonymous User", +// photoURL: user.photoURL || "", +// content: trimmed, +// createdAt: Date.now(), +// edited: false, +// }; +// try { +// await updateDoc(doc(db, "posts", post.id), { +// comments: arrayUnion(newComment), +// }); +// await updateStreak(user.uid); +// setCommentDraft(""); +// await createNotification({ +// toUid: post.uid, +// fromUser: user, +// type: "comment", +// postId: post.id, +// preview: trimmed, +// }); +// } catch (err) { +// console.error(err); +// setError("Failed to add comment."); +// } +// }; + +// const startEditComment = (comment) => { +// setEditingComment({ +// postId: comment._postId, +// createdAt: comment.createdAt, +// }); +// setEditingCommentDraft(comment.content); +// }; +// const cancelEditComment = () => { +// setEditingComment(null); +// setEditingCommentDraft(""); +// }; + +// const handleSaveCommentEdit = async (post, oldComment) => { +// if (!editingCommentDraft.trim()) return; +// const trimmed = editingCommentDraft.trim(); +// const updatedComments = (post.comments || []).map((c) => +// c.createdAt === oldComment.createdAt && c.uid === oldComment.uid +// ? { ...c, content: trimmed, edited: true } +// : c, +// ); +// try { +// await updateDoc(doc(db, "posts", post.id), { comments: updatedComments }); +// setEditingComment(null); +// setEditingCommentDraft(""); +// await createNotification({ +// toUid: post.uid, +// fromUser: user, +// type: "comment_edit", +// postId: post.id, +// preview: trimmed, +// }); +// } catch (err) { +// console.error(err); +// setError("Failed to edit comment."); +// } +// }; + +// const handleDeleteComment = async (post, comment) => { +// if (!window.confirm("Delete this comment?")) return; +// const updatedComments = (post.comments || []).filter( +// (c) => !(c.createdAt === comment.createdAt && c.uid === comment.uid), +// ); +// try { +// await updateDoc(doc(db, "posts", post.id), { comments: updatedComments }); +// } catch (err) { +// console.error(err); +// setError("Failed to delete comment."); +// } +// }; + +// // ── Shared props bundles ────────────────────────────────────────────────── +// const sharedPostProps = { +// user, +// isMobile, +// openCommentsFor, +// commentDraft, +// setCommentDraft, +// editingId, +// editContent, +// setEditContent, +// editingComment, +// editingCommentDraft, +// setEditingCommentDraft, +// savedPostIds, +// getLiveName, +// getLivePhoto, +// onOpenProfile: openProfile, +// onToggleLike: handleToggleLike, +// onToggleSave: handleToggleSave, +// onDeletePost: handleDeletePost, +// onStartEdit: startEdit, +// onCancelEdit: cancelEdit, +// onSaveEdit: handleSaveEdit, +// onToggleComments: toggleComments, +// onAddComment: handleAddComment, +// onStartEditComment: startEditComment, +// onCancelEditComment: cancelEditComment, +// onSaveCommentEdit: handleSaveCommentEdit, +// onDeleteComment: handleDeleteComment, +// onVotePoll: handleVotePoll, +// onToggleCommentReaction: handleToggleCommentReaction, +// }; + +// const composerProps = { +// content, +// setContent, +// postType, +// setPostType, +// selectedTags, +// setSelectedTags, +// customTag, +// setCustomTag, +// showAiDraft, +// setShowAiDraft, +// error, +// onPost: handleCreatePost, +// onOpenCodeEditor: () => setShowCodeEditor(true), +// pollOptions, +// setPollOptions, +// imageURL, +// setImageURL, +// imageUploading, +// setImageUploading, +// }; + +// const feedColumnProps = { +// posts, +// filteredPosts, +// trendingPosts, +// activeTab, +// setActiveTab, +// highlightedPostId, +// ...sharedPostProps, +// ...composerProps, +// }; + +// return ( +// +//
+//
+// + +// {!isMobile && ( +//
+// setShowSavedPosts(true)} +// onShowFeatureTour={() => setShowFeatureTour(true)} +// streak={streak} +// /> + +// + +// +// setActiveTag((prev) => (prev === tag ? null : tag)) +// } +// /> +//
+// )} + +// {isMobile && ( +// setShowFeatureTour(true)} +// feedColumnProps={feedColumnProps} +// /> +// )} +//
+ +// setShowCodeEditor(false)} +// onInsert={handleInsertCode} +// /> +// {showSavedPosts && !isMobile && ( +// setShowSavedPosts(false)} +// onUnsave={(postId) => handleToggleSave(postId)} +// /> +// )} +// {showFeatureTour && } + +// setProfilePopup(null)} +// onFollowToggle={handleFollowToggle} +// onViewProfile={handleViewProfile} +// /> +//
+//
+// ); +// } diff --git a/components/Notifications.js b/components/Notifications.js index 26a97ca..1676dc7 100644 --- a/components/Notifications.js +++ b/components/Notifications.js @@ -135,6 +135,7 @@ const ICONS = { follow: "➕", comment: "💬", comment_edit: "✏️", + solved: "✅", }; function textFor(n) { @@ -147,6 +148,8 @@ function textFor(n) { return <>commented on your post{n.preview ? `: "${n.preview}"` : ""}; case "comment_edit": return <>edited their comment{n.preview ? `: "${n.preview}"` : ""}; + case "solved": + return <>marked your comment as the accepted answer ✅; default: return <>interacted with your content; } @@ -298,4 +301,306 @@ export default function Notifications({ isMobile = false }) { )} ); -} \ No newline at end of file +} + +// "use client"; + +// import { useEffect, useState, useRef, useCallback } from "react"; +// import { useRouter } from "next/navigation"; +// import { useAuth } from "../context/AuthContext"; +// import { db } from "../lib/firebase"; +// import { +// collection, +// query, +// where, +// orderBy, +// limit, +// onSnapshot, +// doc, +// updateDoc, +// deleteDoc, +// writeBatch, +// } from "firebase/firestore"; + +// const S = { +// wrap: { position: "relative", display: "inline-block" }, +// bellBtn: (active) => ({ +// display: "flex", +// alignItems: "center", +// justifyContent: "center", +// width: 40, +// height: 40, +// background: active ? "var(--accent-primary-alpha)" : "transparent", +// border: `1.5px solid ${active ? "var(--accent-primary)" : "var(--border-color)"}`, +// borderRadius: "var(--radius-md)", +// color: active ? "var(--accent-primary)" : "var(--text-secondary)", +// cursor: "pointer", +// fontSize: "1rem", +// position: "relative", +// flexShrink: 0, +// }), +// unreadDot: { +// position: "absolute", +// top: 6, +// right: 6, +// width: 9, +// height: 9, +// borderRadius: "50%", +// backgroundColor: "#3b82f6", +// boxShadow: "0 0 0 2px var(--bg-secondary), 0 0 6px rgba(59,130,246,0.8)", +// }, +// panel: { +// position: "absolute", +// top: "calc(100% + 10px)", +// right: 0, +// width: 340, +// maxHeight: 440, +// overflowY: "auto", +// backgroundColor: "var(--bg-secondary)", +// border: "1px solid var(--border-color)", +// borderRadius: "var(--radius-lg)", +// boxShadow: "0 12px 32px rgba(0,0,0,0.45)", +// zIndex: 200, +// }, +// panelMobile: { +// position: "fixed", +// top: 64, +// left: 8, +// right: 8, +// width: "auto", +// maxHeight: "70vh", +// overflowY: "auto", +// backgroundColor: "var(--bg-secondary)", +// border: "1px solid var(--border-color)", +// borderRadius: "var(--radius-lg)", +// boxShadow: "0 12px 32px rgba(0,0,0,0.45)", +// zIndex: 200, +// }, +// header: { +// display: "flex", +// alignItems: "center", +// justifyContent: "space-between", +// padding: "12px 14px", +// borderBottom: "1px solid var(--border-color)", +// }, +// headerTitle: { fontWeight: 700, fontSize: "0.92rem", color: "var(--text-primary)" }, +// markAllBtn: { +// background: "transparent", +// border: "none", +// color: "var(--accent-primary)", +// fontSize: "0.76rem", +// fontWeight: 600, +// cursor: "pointer", +// fontFamily: "inherit", +// }, +// item: (unread) => ({ +// display: "flex", +// gap: 10, +// alignItems: "flex-start", +// padding: "12px 14px", +// cursor: "pointer", +// borderBottom: "1px solid rgba(255,255,255,0.05)", +// backgroundColor: unread ? "var(--accent-primary-alpha)" : "transparent", +// }), +// avatar: { +// width: 34, +// height: 34, +// borderRadius: "50%", +// background: "linear-gradient(135deg, #0284c7, #38bdf8)", +// display: "flex", +// alignItems: "center", +// justifyContent: "center", +// color: "#000", +// fontWeight: 700, +// fontSize: "0.85rem", +// flexShrink: 0, +// overflow: "hidden", +// }, +// itemText: { fontSize: "0.84rem", color: "var(--text-secondary)", lineHeight: 1.4 }, +// itemName: { fontWeight: 700, color: "var(--text-primary)" }, +// itemMeta: { fontSize: "0.7rem", color: "var(--text-muted)", marginTop: 3 }, +// unreadBall: { +// width: 8, +// height: 8, +// borderRadius: "50%", +// backgroundColor: "#3b82f6", +// flexShrink: 0, +// marginTop: 4, +// }, +// emptyState: { +// padding: "32px 16px", +// textAlign: "center", +// color: "var(--text-muted)", +// fontSize: "0.85rem", +// }, +// }; + +// const ICONS = { +// like: "❤️", +// follow: "➕", +// comment: "💬", +// comment_edit: "✏️", +// }; + +// function textFor(n) { +// switch (n.type) { +// case "like": +// return <>liked your post; +// case "follow": +// return <>started following you; +// case "comment": +// return <>commented on your post{n.preview ? `: "${n.preview}"` : ""}; +// case "comment_edit": +// return <>edited their comment{n.preview ? `: "${n.preview}"` : ""}; +// default: +// return <>interacted with your content; +// } +// } + +// function timeAgo(ts) { +// if (!ts?.toDate) return "Just now"; +// const date = ts.toDate(); +// const diffSec = Math.max(0, (Date.now() - date.getTime()) / 1000); +// if (diffSec < 60) return "Just now"; +// if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`; +// if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`; +// return date.toLocaleDateString(undefined, { month: "short", day: "numeric" }); +// } + +// export default function Notifications({ isMobile = false }) { +// const { user } = useAuth(); +// const router = useRouter(); +// const [open, setOpen] = useState(false); +// const [items, setItems] = useState([]); +// const wrapRef = useRef(null); + +// useEffect(() => { +// if (!user) { +// setItems([]); +// return; +// } +// const q = query( +// collection(db, "notifications"), +// where("toUid", "==", user.uid), +// orderBy("timestamp", "desc"), +// limit(30) +// ); +// const unsub = onSnapshot(q, (snap) => { +// setItems(snap.docs.map((d) => ({ id: d.id, ...d.data() }))); +// }, (err) => console.error("Failed to load notifications:", err)); +// return () => unsub(); +// }, [user]); + +// // Close on outside click +// useEffect(() => { +// if (!open) return; +// const handler = (e) => { +// if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); +// }; +// document.addEventListener("mousedown", handler); +// return () => document.removeEventListener("mousedown", handler); +// }, [open]); + +// const unreadCount = items.filter((n) => !n.read).length; + +// const clearAll = useCallback(async (e) => { +// e.stopPropagation(); +// if (items.length === 0) return; +// const toClear = items; +// setItems([]); +// try { +// const batch = writeBatch(db); +// toClear.forEach((n) => batch.delete(doc(db, "notifications", n.id))); +// await batch.commit(); +// } catch (err) { +// console.error("Failed to clear notifications:", err); +// } +// }, [items]); + +// const handleItemClick = useCallback(async (n) => { +// setOpen(false); +// // Optimistically remove it from the list right away, then delete it +// // server-side so it doesn't come back. +// setItems((prev) => prev.filter((item) => item.id !== n.id)); +// try { await deleteDoc(doc(db, "notifications", n.id)); } +// catch (err) { console.error("Failed to remove notification:", err); } + +// if (n.type === "follow") { +// router.push(`/user/${n.fromUid}`); +// return; +// } + +// if (n.postId) { +// const targetHash = `#post-${n.postId}`; +// const isOnDashboard = window.location.pathname === "/dashboard"; +// if (isOnDashboard) { +// window.location.hash = targetHash; +// window.dispatchEvent(new Event("dashboard-scroll-request")); +// } else { +// router.push(`/dashboard${targetHash}`); +// } +// } +// }, [router]); + +// if (!user) return null; + +// return ( +//
+// + +// {open && ( +//
+//
+// +// Notifications {unreadCount > 0 ? `(${unreadCount})` : ""} +// +// {items.length > 0 && ( +// +// )} +//
+ +// {items.length === 0 ? ( +//
+//
🔔
+// No notifications yet. +//
+// ) : ( +// items.map((n) => ( +//
handleItemClick(n)} +// > +//
+// {n.fromPhoto +// ? {n.fromName} +// : (n.fromName?.charAt(0)?.toUpperCase() || "U")} +//
+//
+//
+// {n.fromName}{" "} +// {textFor(n)} +//
+//
+// {ICONS[n.type] || "🔔"} {timeAgo(n.timestamp)} +//
+//
+// {!n.read && } +//
+// )) +// )} +//
+// )} +//
+// ); +// } \ No newline at end of file diff --git a/components/dashboard/FeedColumn.js b/components/dashboard/FeedColumn.js index c9fb4c5..59f16ed 100644 --- a/components/dashboard/FeedColumn.js +++ b/components/dashboard/FeedColumn.js @@ -104,6 +104,7 @@ export default function FeedColumn({ onDeleteComment, onVotePoll, onToggleCommentReaction, + onMarkSolved, // Composer props content, setContent, @@ -135,6 +136,7 @@ export default function FeedColumn({ onDeleteComment, onVotePoll, onToggleCommentReaction, + onMarkSolved, }; return ( @@ -247,4 +249,255 @@ export default function FeedColumn({ )} ); -} \ No newline at end of file +} + +// "use client"; + +// import PostCard from "./PostCard"; +// import PostComposer from "./PostComposer"; + +// const FEED_TABS = [ +// { id: "latest", label: "Latest Feed", icon: "▦" }, +// { id: "trending", label: "Trending", icon: "📈" }, +// { id: "questions", label: "Questions", icon: "❔" }, +// { id: "collaboration", label: "Collaborate", icon: "👥" }, +// ]; + +// const S = { +// feedColumn: { +// display: "flex", +// flexDirection: "column", +// gap: 16, +// minWidth: 0, +// }, +// feedFiltersBar: { +// display: "flex", +// borderBottom: "1px solid var(--border-color)", +// paddingBottom: 2, +// gap: 4, +// overflowX: "auto", +// WebkitOverflowScrolling: "touch", +// scrollbarWidth: "none", +// }, +// filterTab: { +// padding: "8px 14px", +// borderWidth: 0, +// borderStyle: "solid", +// borderColor: "transparent", +// borderBottomWidth: 2, +// borderBottomColor: "transparent", +// background: "transparent", +// color: "var(--text-muted)", +// fontWeight: 500, +// fontSize: "0.85rem", +// cursor: "pointer", +// whiteSpace: "nowrap", +// fontFamily: "inherit", +// }, +// filterTabActive: { +// padding: "8px 14px", +// borderWidth: 0, +// borderStyle: "solid", +// borderColor: "transparent", +// borderBottomWidth: 2, +// borderBottomColor: "var(--accent-primary)", +// background: "transparent", +// color: "var(--accent-primary)", +// fontWeight: 600, +// fontSize: "0.85rem", +// cursor: "pointer", +// whiteSpace: "nowrap", +// fontFamily: "inherit", +// }, +// emptyState: { +// display: "flex", +// flexDirection: "column", +// alignItems: "center", +// justifyContent: "center", +// gap: 12, +// padding: "48px 24px", +// color: "var(--text-muted)", +// textAlign: "center", +// }, +// }; + +// export default function FeedColumn({ +// user, +// posts, +// filteredPosts, +// trendingPosts, +// activeTab, +// setActiveTab, +// isMobile, +// highlightedPostId, +// openCommentsFor, +// commentDraft, +// setCommentDraft, +// editingId, +// editContent, +// setEditContent, +// editingComment, +// editingCommentDraft, +// setEditingCommentDraft, +// savedPostIds, +// getLiveName, +// getLivePhoto, +// onOpenProfile, +// onToggleLike, +// onToggleSave, +// onDeletePost, +// onStartEdit, +// onCancelEdit, +// onSaveEdit, +// onToggleComments, +// onAddComment, +// onStartEditComment, +// onCancelEditComment, +// onSaveCommentEdit, +// onDeleteComment, +// onVotePoll, +// onToggleCommentReaction, +// // Composer props +// content, +// setContent, +// postType, +// setPostType, +// selectedTags, +// setSelectedTags, +// customTag, +// setCustomTag, +// showAiDraft, +// setShowAiDraft, +// error, +// onPost, +// onOpenCodeEditor, +// pollOptions, +// setPollOptions, +// imageURL, +// setImageURL, +// imageUploading, +// setImageUploading, +// }) { +// const postCardProps = { +// user, isMobile, openCommentsFor, commentDraft, setCommentDraft, +// editingId, editContent, setEditContent, editingComment, +// editingCommentDraft, setEditingCommentDraft, savedPostIds, +// getLiveName, getLivePhoto, onOpenProfile, onToggleLike, onToggleSave, +// onDeletePost, onStartEdit, onCancelEdit, onSaveEdit, onToggleComments, +// onAddComment, onStartEditComment, onCancelEditComment, onSaveCommentEdit, +// onDeleteComment, +// onVotePoll, +// onToggleCommentReaction, +// }; + +// return ( +//
+// + +//
+// {FEED_TABS.map(({ id, label }) => ( +// +// ))} +//
+ +// {activeTab === "trending" && ( +//
+//
+// 🔥 Most liked posts in the last 48 hours +//
+// {trendingPosts.length === 0 ? ( +//
+//
📭
+//
No trending posts yet
+//
Posts liked in the last 48 hours will appear here.
+//
+// ) : ( +// trendingPosts.map((post, i) => ( +// +// )) +// )} +//
+// )} + +// {activeTab === "questions" && ( +//
+//
+// ❔ Questions from the community — help someone out! +//
+// {filteredPosts.length === 0 ? ( +//
+//
🙋
+//
No questions yet
+//
Post a question using the composer above.
+//
+// ) : ( +// filteredPosts.map((post, i) => ( +// +// )) +// )} +//
+// )} + +// {activeTab === "collaboration" && ( +//
+//
+// 🤝 Find developers to build something together +//
+// {filteredPosts.length === 0 ? ( +//
+//
🚀
+//
No collaboration posts yet
+//
Post a collaboration request using the composer above.
+//
+// ) : ( +// filteredPosts.map((post, i) => ( +// +// )) +// )} +//
+// )} + +// {activeTab === "latest" && ( +//
+// {filteredPosts.length === 0 ? ( +//

No posts found for this tag.

+// ) : ( +// filteredPosts.map((post, i) => ( +// +// )) +// )} +//
+// )} +//
+// ); +// } \ No newline at end of file diff --git a/components/dashboard/PostCard.js b/components/dashboard/PostCard.js index b01db46..a3e7a3f 100644 --- a/components/dashboard/PostCard.js +++ b/components/dashboard/PostCard.js @@ -63,6 +63,50 @@ const S = { textTransform: "uppercase", whiteSpace: "nowrap", }), + solvedBadge: { + display: "inline-flex", + alignItems: "center", + gap: 4, + padding: "2px 8px", + backgroundColor: "rgba(52,211,153,0.12)", + border: "1px solid rgba(52,211,153,0.4)", + color: "#34d399", + borderRadius: "var(--radius-sm)", + fontSize: "0.7rem", + fontWeight: 600, + textTransform: "uppercase", + whiteSpace: "nowrap", + }, + acceptedCommentWrapper: { + border: "1px solid rgba(52,211,153,0.35)", + borderRadius: "var(--radius-md)", + backgroundColor: "rgba(52,211,153,0.05)", + padding: "8px 10px", + }, + acceptedLabel: { + display: "inline-flex", + alignItems: "center", + gap: 4, + fontSize: "0.72rem", + fontWeight: 600, + color: "#34d399", + marginBottom: 4, + }, + btnAccept: (isAccepted) => ({ + display: "flex", + alignItems: "center", + gap: 3, + padding: "2px 8px", + fontSize: "0.72rem", + fontWeight: 600, + border: isAccepted ? "1px solid rgba(52,211,153,0.6)" : "1px solid var(--border-color)", + borderRadius: "var(--radius-sm)", + backgroundColor: isAccepted ? "rgba(52,211,153,0.12)" : "transparent", + color: isAccepted ? "#34d399" : "var(--text-muted)", + cursor: "pointer", + transition: "all 0.15s", + whiteSpace: "nowrap", + }), postBody: { fontSize: "0.9rem", color: "var(--text-secondary)" }, pollQuestion: { fontSize: "0.95rem", @@ -450,6 +494,7 @@ export default function PostCard({ onDeleteComment, onVotePoll, onToggleCommentReaction, + onMarkSolved, }) { const [openPickerFor, setOpenPickerFor] = useState(null); @@ -495,7 +540,12 @@ export default function PostCard({
- {typeLabel} +
+ {typeLabel} + {type === "question" && post.solved && ( + ✓ Solved + )} +
{post.timestamp?.toDate ? post.timestamp.toDate().toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) @@ -602,13 +652,24 @@ export default function PostCard({ const commentName = getLiveName(c.uid, c.displayName); const commentKey = `${c.uid}-${c.createdAt}`; const isPickerOpen = openPickerFor === commentKey; + const isAccepted = post.solved && post.solvedCommentId === c.createdAt; + const isPostAuthor = user?.uid === post.uid; + const isQuestion = post.postType === "question"; // extra emojis that already have at least one reaction (show inline) const activeExtra = Object.keys(c.reactions || {}) .filter((e) => !DEFAULT_EMOJIS.includes(e) && (c.reactions[e]?.length || 0) > 0); return ( -
+
+ {/* Accepted answer label */} + {isAccepted && ( +
✓ Accepted Answer
+ )} + {/* Comment header */}
@@ -629,6 +690,15 @@ export default function PostCard({
{new Date(c.createdAt).toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })} {c.edited && (edited)} + {isQuestion && isPostAuthor && !isEditingThis && ( + + )} {isOwner && !isEditingThis && ( <> @@ -754,4 +824,762 @@ export default function PostCard({ )} ); -} \ No newline at end of file +} + +// "use client"; + +// import ReactMarkdown from "react-markdown"; +// import remarkGfm from "remark-gfm"; +// import { useState, useEffect } from "react"; +// import UserAvatar from "./UserAvatar"; + +// const DEFAULT_EMOJIS = ["👍", "✅", "💡", "❤️"]; +// const EXTRA_EMOJIS = ["🔥", "😂", "😮", "🎉", "🙌", "💯", "🚀", "😢", "😡", "👀", "🤔", "⭐"]; + +// const S = { +// discussionCard: { +// backgroundColor: "var(--bg-secondary)", +// borderWidth: "1px", +// borderStyle: "solid", +// borderColor: "var(--border-color)", +// borderRadius: "var(--radius-lg)", +// padding: 16, +// boxShadow: "var(--shadow-sm)", +// display: "flex", +// flexDirection: "column", +// gap: 12, +// transition: "box-shadow 0.4s ease, border-color 0.4s ease", +// }, +// discussionCardHighlighted: { +// boxShadow: "0 0 0 2px var(--accent-primary), var(--shadow-sm)", +// borderColor: "var(--accent-primary)", +// }, +// cardHeader: { +// display: "flex", +// alignItems: "flex-start", +// justifyContent: "space-between", +// gap: 8, +// }, +// authorInfo: { display: "flex", alignItems: "center", gap: 10 }, +// authorMeta: { display: "flex", flexDirection: "column" }, +// authorName: { color: "var(--text-primary)", fontWeight: 600, fontSize: "0.9rem" }, +// authorTitle: { color: "var(--text-muted)", fontSize: "0.72rem" }, +// postTimestamp: { color: "var(--text-muted)", fontSize: "0.75rem", textAlign: "right" }, +// categoryTag: (type) => ({ +// display: "inline-flex", +// alignItems: "center", +// padding: "2px 8px", +// backgroundColor: +// type === "question" ? "rgba(251,146,60,0.12)" +// : type === "collaboration" ? "rgba(52,211,153,0.12)" +// : type === "poll" ? "rgba(99,102,241,0.12)" +// : "var(--bg-primary)", +// border: `1px solid ${ +// type === "question" ? "rgba(251,146,60,0.4)" +// : type === "collaboration" ? "rgba(52,211,153,0.4)" +// : type === "poll" ? "rgba(99,102,241,0.4)" +// : "var(--border-color)" +// }`, +// color: +// type === "question" ? "#fb923c" +// : type === "collaboration" ? "#34d399" +// : type === "poll" ? "#818cf8" +// : "var(--text-secondary)", +// borderRadius: "var(--radius-sm)", +// fontSize: "0.7rem", +// fontWeight: 600, +// textTransform: "uppercase", +// whiteSpace: "nowrap", +// }), +// postBody: { fontSize: "0.9rem", color: "var(--text-secondary)" }, +// pollQuestion: { +// fontSize: "0.95rem", +// fontWeight: 600, +// color: "var(--text-primary)", +// lineHeight: 1.5, +// marginBottom: 2, +// }, +// postImage: { +// display: "block", +// width: "100%", +// maxHeight: 360, +// objectFit: "cover", +// borderRadius: "var(--radius-md)", +// border: "1px solid var(--border-color)", +// marginTop: 8, +// }, +// postTags: { display: "flex", flexWrap: "wrap", gap: 6, marginTop: 2 }, +// postTag: { color: "var(--accent-primary)", fontSize: "0.82rem", fontWeight: 500 }, +// postActions: { +// display: "flex", +// alignItems: "center", +// justifyContent: "space-between", +// borderTop: "1px solid rgba(255,255,255,0.05)", +// padding: "6px 2px", +// marginTop: 2, +// gap: 4, +// flexWrap: "wrap", +// }, +// postActionsGroup: { display: "flex", gap: 6 }, +// btnAction: { +// display: "flex", +// alignItems: "center", +// gap: 4, +// background: "transparent", +// border: "none", +// color: "var(--text-muted)", +// cursor: "pointer", +// fontSize: "0.82rem", +// fontWeight: 500, +// padding: "6px 8px", +// borderRadius: "var(--radius-sm)", +// fontFamily: "inherit", +// }, +// btnActionActive: { +// display: "flex", +// alignItems: "center", +// gap: 4, +// background: "transparent", +// border: "none", +// color: "var(--accent-primary)", +// cursor: "pointer", +// fontSize: "0.82rem", +// fontWeight: 600, +// padding: "6px 8px", +// borderRadius: "var(--radius-sm)", +// fontFamily: "inherit", +// }, +// btnActionDanger: { +// display: "flex", +// alignItems: "center", +// gap: 4, +// background: "transparent", +// border: "none", +// color: "#f87171", +// cursor: "pointer", +// fontSize: "0.78rem", +// fontWeight: 500, +// padding: "4px 6px", +// borderRadius: "var(--radius-sm)", +// fontFamily: "inherit", +// }, +// composerTextarea: { +// width: "100%", +// minHeight: 72, +// background: "transparent", +// border: "none", +// resize: "vertical", +// color: "var(--text-primary)", +// outline: "none", +// fontSize: "0.95rem", +// fontFamily: "inherit", +// }, +// btnPost: { +// display: "flex", +// alignItems: "center", +// gap: 8, +// padding: "8px 16px", +// backgroundColor: "var(--accent-primary)", +// border: "none", +// borderRadius: "var(--radius-md)", +// color: "#000", +// fontWeight: 600, +// cursor: "pointer", +// fontSize: "0.9rem", +// }, +// btnPostDisabled: { +// display: "flex", +// alignItems: "center", +// gap: 8, +// padding: "8px 16px", +// backgroundColor: "var(--border-color)", +// border: "none", +// borderRadius: "var(--radius-md)", +// color: "var(--text-muted)", +// fontWeight: 600, +// cursor: "not-allowed", +// fontSize: "0.9rem", +// }, +// commentItem: { +// display: "flex", +// flexDirection: "column", +// gap: 4, +// padding: "10px 12px", +// backgroundColor: "var(--bg-primary)", +// borderRadius: "var(--radius-md)", +// border: "1px solid var(--border-color)", +// }, +// commentHeader: { +// display: "flex", +// alignItems: "center", +// justifyContent: "space-between", +// gap: 8, +// }, +// commentAuthor: { fontWeight: 600, fontSize: "0.82rem", color: "var(--text-primary)" }, +// commentMeta: { +// display: "flex", +// alignItems: "center", +// gap: 4, +// fontSize: "0.72rem", +// color: "var(--text-muted)", +// flexWrap: "wrap", +// }, +// commentBody: { fontSize: "0.85rem", color: "var(--text-secondary)", lineHeight: 1.5 }, +// commentEditTextarea: { +// width: "100%", +// background: "var(--bg-secondary)", +// border: "1px solid var(--border-color)", +// borderRadius: "var(--radius-sm)", +// color: "var(--text-primary)", +// outline: "none", +// fontSize: "0.875rem", +// fontFamily: "inherit", +// padding: "6px 8px", +// resize: "vertical", +// minHeight: 60, +// boxSizing: "border-box", +// }, +// commentEditActions: { display: "flex", gap: 6, marginTop: 4 }, +// btnSm: { +// padding: "4px 12px", +// backgroundColor: "var(--accent-primary)", +// border: "none", +// borderRadius: "var(--radius-sm)", +// color: "#000", +// fontWeight: 600, +// fontSize: "0.8rem", +// cursor: "pointer", +// fontFamily: "inherit", +// }, +// btnSmGhost: { +// padding: "4px 12px", +// backgroundColor: "transparent", +// border: "1px solid var(--border-color)", +// borderRadius: "var(--radius-sm)", +// color: "var(--text-muted)", +// fontWeight: 500, +// fontSize: "0.8rem", +// cursor: "pointer", +// fontFamily: "inherit", +// }, +// // ── Poll styles ─────────────────────────────────────────────────────────── +// pollContainer: { +// display: "flex", +// flexDirection: "column", +// gap: 8, +// }, +// pollOptionBtn: (voted) => ({ +// width: "100%", +// textAlign: "left", +// background: "none", +// border: `1px solid ${voted ? "var(--accent-primary)" : "var(--border-color)"}`, +// borderRadius: "var(--radius-md)", +// cursor: "pointer", +// padding: 0, +// overflow: "hidden", +// fontFamily: "inherit", +// position: "relative", +// }), +// pollBarFill: (pct, voted) => ({ +// position: "absolute", +// inset: 0, +// width: `${pct}%`, +// background: voted ? "var(--accent-primary-alpha)" : "rgba(255,255,255,0.04)", +// borderRadius: "var(--radius-md)", +// transition: "width 0.4s ease", +// }), +// pollOptionLabel: { +// position: "relative", +// display: "flex", +// alignItems: "center", +// justifyContent: "space-between", +// padding: "9px 12px", +// fontSize: "0.85rem", +// color: "var(--text-primary)", +// fontWeight: 500, +// zIndex: 1, +// }, +// pollPct: { +// fontSize: "0.75rem", +// fontWeight: 700, +// color: "var(--text-muted)", +// minWidth: 32, +// textAlign: "right", +// }, +// pollMeta: { +// fontSize: "0.72rem", +// color: "var(--text-muted)", +// marginTop: 2, +// }, +// // ── Reaction styles ─────────────────────────────────────────────────────── +// reactionBtn: (hasReacted) => ({ +// display: "flex", +// alignItems: "center", +// gap: 3, +// padding: "2px 8px", +// borderRadius: 999, +// border: hasReacted ? "1px solid var(--accent-primary)" : "1px solid var(--border-color)", +// background: hasReacted ? "rgba(99,102,241,0.12)" : "transparent", +// color: hasReacted ? "var(--accent-primary)" : "var(--text-muted)", +// fontSize: "0.78rem", +// fontWeight: hasReacted ? 600 : 400, +// cursor: "pointer", +// fontFamily: "inherit", +// transition: "all 0.15s ease", +// lineHeight: 1.4, +// }), +// pickerToggleBtn: (isOpen) => ({ +// display: "flex", +// alignItems: "center", +// justifyContent: "center", +// padding: "2px 8px", +// height: 22, +// borderRadius: 999, +// border: "1px solid var(--border-color)", +// background: isOpen ? "var(--accent-primary-alpha)" : "transparent", +// color: isOpen ? "var(--accent-primary)" : "var(--text-muted)", +// fontSize: "0.72rem", +// fontWeight: 600, +// cursor: "pointer", +// fontFamily: "inherit", +// transition: "all 0.15s", +// whiteSpace: "nowrap", +// gap: 3, +// }), +// pickerGrid: { +// position: "absolute", +// bottom: "calc(100% + 8px)", +// left: 0, +// zIndex: 50, +// display: "grid", +// gridTemplateColumns: "repeat(6, 1fr)", +// gap: 4, +// padding: 10, +// backgroundColor: "var(--bg-secondary)", +// border: "1px solid var(--border-color)", +// borderRadius: "var(--radius-md)", +// boxShadow: "0 4px 24px rgba(0,0,0,0.4)", +// minWidth: 210, +// }, +// pickerEmojiBtn: (hasReacted) => ({ +// position: "relative", +// display: "flex", +// alignItems: "center", +// justifyContent: "center", +// width: 32, +// height: 32, +// borderRadius: "var(--radius-sm)", +// border: "none", +// background: hasReacted ? "var(--accent-primary-alpha)" : "transparent", +// fontSize: "1.1rem", +// cursor: "pointer", +// transition: "background 0.12s", +// }), +// pickerBadge: { +// position: "absolute", +// top: 0, +// right: 0, +// fontSize: "0.52rem", +// fontWeight: 700, +// color: "var(--accent-primary)", +// backgroundColor: "var(--bg-secondary)", +// borderRadius: 999, +// padding: "0 2px", +// lineHeight: 1.4, +// pointerEvents: "none", +// }, +// }; + +// // ── Poll block ──────────────────────────────────────────────────────────────── +// function PollBlock({ post, user, onVotePoll }) { +// const options = post.pollOptions || []; +// const votes = post.pollVotes || {}; +// const totalVotes = Object.values(votes).reduce((s, arr) => s + (arr?.length || 0), 0); + +// const userVotedIdx = options.findIndex((_, idx) => +// (votes[idx] || []).includes(user?.uid) +// ); +// const hasVoted = userVotedIdx !== -1; + +// return ( +//
+// {/* Poll question */} +// {post.content && ( +//

{post.content}

+// )} + +// {/* Options */} +// {options.map((opt, idx) => { +// const count = (votes[idx] || []).length; +// const pct = totalVotes > 0 ? Math.round((count / totalVotes) * 100) : 0; +// const isMyVote = userVotedIdx === idx; + +// return ( +// +// ); +// })} + +//
+// {totalVotes} vote{totalVotes !== 1 ? "s" : ""} +// {hasVoted && " · You voted"} +//
+//
+// ); +// } + +// // ── PostCard ────────────────────────────────────────────────────────────────── +// export default function PostCard({ +// post, +// postIndex, +// user, +// isMobile, +// isHighlighted, +// openCommentsFor, +// commentDraft, +// setCommentDraft, +// editingId, +// editContent, +// setEditContent, +// editingComment, +// editingCommentDraft, +// setEditingCommentDraft, +// savedPostIds, +// getLiveName, +// getLivePhoto, +// onOpenProfile, +// onToggleLike, +// onToggleSave, +// onDeletePost, +// onStartEdit, +// onCancelEdit, +// onSaveEdit, +// onToggleComments, +// onAddComment, +// onStartEditComment, +// onCancelEditComment, +// onSaveCommentEdit, +// onDeleteComment, +// onVotePoll, +// onToggleCommentReaction, +// }) { +// const [openPickerFor, setOpenPickerFor] = useState(null); + +// useEffect(() => { +// if (!openPickerFor) return; +// const close = () => setOpenPickerFor(null); +// window.addEventListener("click", close); +// return () => window.removeEventListener("click", close); +// }, [openPickerFor]); + +// const postPhoto = getLivePhoto(post.uid, post.photoURL); +// const postName = getLiveName(post.uid, post.displayName); +// const type = post.postType || "discussion"; +// const typeLabel = +// type === "question" ? "Question" +// : type === "collaboration" ? "Collaborate" +// : type === "poll" ? "Poll" +// : "Discussion"; + +// return ( +//
+// {/* ── Card Header ── */} +//
+//
+// onOpenProfile(e, post.uid, post.displayName, post.photoURL)} +// /> +//
+// onOpenProfile(e, post.uid, post.displayName, post.photoURL)} +// > +// {postName} +// +// Community Member +//
+//
+//
+// {typeLabel} +// +// {post.timestamp?.toDate +// ? post.timestamp.toDate().toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) +// : "Just now"} +// {post.edited ? " (edited)" : ""} +// +//
+//
+ +// {/* ── Post Body / Poll / Edit Mode ── */} +// {editingId === post.id ? ( +//
+//