From a589521fb1addb09fa00b12ca4cba77d935eca95 Mon Sep 17 00:00:00 2001 From: Vikas K Date: Sun, 21 Jun 2026 00:02:59 +0530 Subject: [PATCH] feat: Add Poll as a new Post type with voting and bar chart results --- app/dashboard/page.js | 61 ++++++++++- components/dashboard/FeedColumn.js | 6 ++ components/dashboard/PostCard.js | 156 ++++++++++++++++++++++++--- components/dashboard/PostComposer.js | 123 ++++++++++++++++++++- 4 files changed, 325 insertions(+), 21 deletions(-) diff --git a/app/dashboard/page.js b/app/dashboard/page.js index 4ee79a4..cf1425f 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([]); @@ -351,9 +352,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 || "", @@ -362,9 +371,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."); @@ -416,6 +434,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)); @@ -491,6 +543,7 @@ export default function Dashboard() { onCancelEditComment: cancelEditComment, onSaveCommentEdit: handleSaveCommentEdit, onDeleteComment: handleDeleteComment, + onVotePoll: handleVotePoll, }; const composerProps = { @@ -499,6 +552,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 87adb46..33dad2c 100644 --- a/components/dashboard/FeedColumn.js +++ b/components/dashboard/FeedColumn.js @@ -102,6 +102,7 @@ export default function FeedColumn({ onCancelEditComment, onSaveCommentEdit, onDeleteComment, + onVotePoll, // Composer props content, setContent, @@ -117,6 +118,8 @@ export default function FeedColumn({ error, onPost, onOpenCodeEditor, + pollOptions, + setPollOptions, }) { const postCardProps = { user, isMobile, openCommentsFor, commentDraft, setCommentDraft, @@ -126,6 +129,7 @@ export default function FeedColumn({ onDeletePost, onStartEdit, onCancelEdit, onSaveEdit, onToggleComments, onAddComment, onStartEditComment, onCancelEditComment, onSaveCommentEdit, onDeleteComment, + onVotePoll, }; return ( @@ -149,6 +153,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 7bf8ab4..6d94bcb 100644 --- a/components/dashboard/PostCard.js +++ b/components/dashboard/PostCard.js @@ -37,13 +37,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, @@ -51,6 +60,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: { @@ -205,8 +221,111 @@ 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, + }, }; +// ── 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 text */} + {post.content && ( +

{post.content}

+ )} + + {/* Poll 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"} +
+
+ ); +} + export default function PostCard({ post, postIndex, @@ -238,17 +357,21 @@ export default function PostCard({ onCancelEditComment, onSaveCommentEdit, onDeleteComment, + onVotePoll, }) { 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 (
{/* Card Header */}
@@ -281,7 +404,7 @@ export default function PostCard({
- {/* Post Body or Edit Mode */} + {/* Post Body / Poll / Edit Mode */} {editingId === post.id ? (