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 ? (
+ ) : type === "poll" ? (
+
) : (
-
+ {type !== "poll" && (
+
+ )}
@@ -435,7 +563,9 @@ export default function PostCard({
{user?.uid === post.uid && (
-
+ {type !== "poll" && (
+
+ )}
)}
@@ -454,7 +584,7 @@ export default function PostCard({
const commentKey = `${c.uid}-${c.createdAt}`;
const isPickerOpen = openPickerFor === commentKey;
- // extra emojis that already have reactions (show them inline too)
+ // 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);
@@ -510,12 +640,10 @@ export default function PostCard({
{/* ── Reaction Bar ── */}
e.stopPropagation()}
>
-
- {/* Default 4 + any active extra emojis inline */}
+ {/* Default 4 + any active extra emojis */}
{[...DEFAULT_EMOJIS, ...activeExtra].map((emoji) => {
const reactors = c.reactions?.[emoji] || [];
const hasReacted = reactors.includes(user?.uid);
@@ -534,7 +662,7 @@ export default function PostCard({
);
})}
- {/* Picker toggle button */}
+ {/* Picker toggle */}
- {/* ── Expanded emoji picker grid ── */}
+ {/* Expanded emoji picker */}
{isPickerOpen && (
{EXTRA_EMOJIS.map((emoji) => {
@@ -557,7 +685,7 @@ export default function PostCard({
title={`${emoji}${reactors.length > 0 ? ` · ${reactors.length}` : ""}`}
onClick={() => {
onToggleCommentReaction?.(post, c, emoji);
- setOpenPickerFor(null); // close after picking
+ setOpenPickerFor(null);
}}
>
{emoji}
diff --git a/components/dashboard/PostComposer.js b/components/dashboard/PostComposer.js
index 33c8734..6dfc9f6 100644
--- a/components/dashboard/PostComposer.js
+++ b/components/dashboard/PostComposer.js
@@ -1,5 +1,6 @@
"use client";
+import { useState } from "react";
import AIDraftAssistant from "../AIDraftAssistant";
import UserAvatar from "./UserAvatar";
@@ -32,6 +33,7 @@ const S = {
gap: 8,
paddingTop: 10,
borderTop: "1px solid rgba(255,255,255,0.05)",
+ flexWrap: "wrap",
},
postTypeBtn: (active) => ({
display: "flex",
@@ -147,6 +149,53 @@ const S = {
cursor: "not-allowed",
fontSize: "0.9rem",
},
+ // Poll builder styles
+ pollBuilder: {
+ display: "flex",
+ flexDirection: "column",
+ gap: 8,
+ padding: 12,
+ background: "var(--bg-primary)",
+ borderRadius: "var(--radius-md)",
+ border: "1px solid var(--border-color)",
+ },
+ pollOptionRow: {
+ display: "flex",
+ alignItems: "center",
+ gap: 8,
+ },
+ pollInput: {
+ flex: 1,
+ padding: "7px 10px",
+ background: "var(--bg-secondary)",
+ border: "1px solid var(--border-color)",
+ borderRadius: "var(--radius-md)",
+ color: "var(--text-primary)",
+ fontSize: "0.85rem",
+ fontFamily: "inherit",
+ outline: "none",
+ },
+ btnAddOption: {
+ padding: "6px 12px",
+ background: "transparent",
+ border: "1px dashed var(--border-color)",
+ borderRadius: "var(--radius-md)",
+ color: "var(--text-muted)",
+ fontSize: "0.8rem",
+ cursor: "pointer",
+ fontFamily: "inherit",
+ textAlign: "left",
+ },
+ btnRemoveOption: {
+ background: "none",
+ border: "none",
+ cursor: "pointer",
+ color: "var(--text-muted)",
+ fontSize: "1rem",
+ padding: "0 4px",
+ lineHeight: 1,
+ flexShrink: 0,
+ },
};
const AVAILABLE_TAGS = [
@@ -158,8 +207,9 @@ const AVAILABLE_TAGS = [
const POST_TYPES = [
{ id: "discussion", label: "💬 Discussion" },
- { id: "question", label: "❔ Question" },
+ { id: "question", label: "❔ Question" },
{ id: "collaboration", label: "🤝 Collaborate" },
+ { id: "poll", label: "📊 Poll" },
];
export default function PostComposer({
@@ -181,6 +231,9 @@ export default function PostComposer({
onOpenCodeEditor,
getLivePhoto,
getLiveName,
+ // Poll props
+ pollOptions,
+ setPollOptions,
}) {
const toggleTag = (tag) =>
setSelectedTags((prev) =>
@@ -196,6 +249,22 @@ export default function PostComposer({
setCustomTag("");
};
+ const updatePollOption = (idx, val) =>
+ setPollOptions((prev) => prev.map((o, i) => i === idx ? val : o));
+
+ const removePollOption = (idx) =>
+ setPollOptions((prev) => prev.filter((_, i) => i !== idx));
+
+ const addPollOption = () => {
+ if (pollOptions.length >= 6) return;
+ setPollOptions((prev) => [...prev, ""]);
+ };
+
+ // Poll is postable if question + at least 2 non-empty options
+ const pollReady = postType === "poll"
+ ? content.trim().length > 0 && pollOptions.filter((o) => o.trim()).length >= 2
+ : content.trim().length > 0;
+
return (
@@ -207,7 +276,11 @@ export default function PostComposer({