diff --git a/app/dashboard/page.js b/app/dashboard/page.js index 6f6ab98..f40c0c1 100644 --- a/app/dashboard/page.js +++ b/app/dashboard/page.js @@ -87,6 +87,7 @@ export default function Dashboard() { const [showAiDraft, setShowAiDraft] = useState(false); const [showCodeEditor, setShowCodeEditor] = useState(false); const [error, setError] = useState(""); + const [pollOptions, setPollOptions] = useState(["", ""]); // ── Feed / UI state ────────────────────────────────────────────────────── const [posts, setPosts] = useState([]); @@ -354,9 +355,17 @@ export default function Dashboard() { // ── Post CRUD ───────────────────────────────────────────────────────────── 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 { setPosting(true); setError(""); - await addDoc(collection(db, "posts"), { + const postData = { uid: user.uid, displayName: user.displayName || user.email || "Anonymous User", photoURL: user.photoURL || "", @@ -365,9 +374,18 @@ export default function Dashboard() { postType, timestamp: serverTimestamp(), likes: 0, likedBy: [], comments: [], - }); + }; + // Attach poll data if poll type + if (postType === "poll") { + postData.pollOptions = pollOptions.filter((o) => o.trim()); + postData.pollVotes = {}; // { "0": [], "1": [], ... } + } + await addDoc(collection(db, "posts"), postData); await updateStreak(user.uid); - setContent(""); setSelectedTags([]); setPostType("discussion"); + setContent(""); + setSelectedTags([]); + setPostType("discussion"); + setPollOptions(["", ""]); } catch (err) { console.error(err); setError("Failed to create post. Please try again."); @@ -425,6 +443,40 @@ export default function Dashboard() { } 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)); @@ -500,6 +552,7 @@ export default function Dashboard() { onCancelEditComment: cancelEditComment, onSaveCommentEdit: handleSaveCommentEdit, onDeleteComment: handleDeleteComment, + onVotePoll: handleVotePoll, }; const composerProps = { @@ -508,6 +561,8 @@ export default function Dashboard() { showAiDraft, setShowAiDraft, posting, error, onPost: handleCreatePost, onOpenCodeEditor: () => setShowCodeEditor(true), + pollOptions, + setPollOptions, }; const feedColumnProps = { diff --git a/components/dashboard/FeedColumn.js b/components/dashboard/FeedColumn.js index 9a50d43..80fe66b 100644 --- a/components/dashboard/FeedColumn.js +++ b/components/dashboard/FeedColumn.js @@ -102,7 +102,8 @@ export default function FeedColumn({ onCancelEditComment, onSaveCommentEdit, onDeleteComment, - onToggleCommentReaction, // ← new prop + onVotePoll, + onToggleCommentReaction, // Composer props content, setContent, @@ -118,6 +119,8 @@ export default function FeedColumn({ error, onPost, onOpenCodeEditor, + pollOptions, + setPollOptions, }) { const postCardProps = { user, isMobile, openCommentsFor, commentDraft, setCommentDraft, @@ -127,7 +130,8 @@ export default function FeedColumn({ onDeletePost, onStartEdit, onCancelEdit, onSaveEdit, onToggleComments, onAddComment, onStartEditComment, onCancelEditComment, onSaveCommentEdit, onDeleteComment, - onToggleCommentReaction, // ← forwarded + onVotePoll, + onToggleCommentReaction, }; return ( @@ -151,6 +155,8 @@ export default function FeedColumn({ onOpenCodeEditor={onOpenCodeEditor} getLivePhoto={getLivePhoto} getLiveName={getLiveName} + pollOptions={pollOptions} + setPollOptions={setPollOptions} />
diff --git a/components/dashboard/PostCard.js b/components/dashboard/PostCard.js index 528c2a6..fe47bec 100644 --- a/components/dashboard/PostCard.js +++ b/components/dashboard/PostCard.js @@ -41,13 +41,22 @@ const S = { 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)" + 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)" : "var(--border-color)"}`, - color: type === "question" ? "#fb923c" : type === "collaboration" ? "#34d399" : "var(--text-secondary)", + 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, @@ -55,6 +64,13 @@ const S = { 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, + }, postTags: { display: "flex", flexWrap: "wrap", gap: 6, marginTop: 2 }, postTag: { color: "var(--accent-primary)", fontSize: "0.82rem", fontWeight: 500 }, postActions: { @@ -209,6 +225,56 @@ const S = { 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", @@ -287,6 +353,61 @@ const S = { }, }; +// ── 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, @@ -318,12 +439,11 @@ export default function PostCard({ onCancelEditComment, onSaveCommentEdit, onDeleteComment, + onVotePoll, onToggleCommentReaction, }) { - // tracks which comment's extra-emoji picker is open (keyed by "uid-createdAt") const [openPickerFor, setOpenPickerFor] = useState(null); - // close picker when clicking anywhere outside useEffect(() => { if (!openPickerFor) return; const close = () => setOpenPickerFor(null); @@ -334,7 +454,11 @@ export default function PostCard({ 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" : "Discussion"; + const typeLabel = + type === "question" ? "Question" + : type === "collaboration" ? "Collaborate" + : type === "poll" ? "Poll" + : "Discussion"; return (
- {/* ── Post Body / Edit Mode ── */} + {/* ── Post Body / Poll / Edit Mode ── */} {editingId === post.id ? (