diff --git a/src/components/post/poll-composer.tsx b/src/components/post/poll-composer.tsx new file mode 100644 index 0000000..fbd0ae7 --- /dev/null +++ b/src/components/post/poll-composer.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { X, Plus, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import type { PollDuration } from "@/lib/types"; + +interface PollComposerProps { + options: string[]; + duration: PollDuration; + onChange: (poll: { options: string[]; duration: PollDuration }) => void; + onRemove: () => void; +} + +const DURATION_LABELS: Record = { + "1d": "1 dia", + "3d": "3 dias", + "7d": "7 dias", +}; + +export function PollComposer({ options, duration, onChange, onRemove }: PollComposerProps) { + const updateOption = (index: number, value: string) => { + const updated = [...options]; + updated[index] = value; + onChange({ options: updated, duration }); + }; + + const addOption = () => { + if (options.length >= 4) return; + onChange({ options: [...options, ""], duration }); + }; + + const removeOption = (index: number) => { + if (options.length <= 2) return; + onChange({ options: options.filter((_, i) => i !== index), duration }); + }; + + return ( +
+
+ {options.map((option, i) => ( +
+ updateOption(i, e.target.value)} + placeholder={`Opcion ${i + 1}`} + maxLength={80} + className="flex-1 rounded-lg border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-xcion-primary" + /> + {options.length > 2 && ( + + )} +
+ ))} +
+ + {options.length < 4 && ( + + )} + +
+ Duracion: +
+ {(Object.keys(DURATION_LABELS) as PollDuration[]).map((d) => ( + + ))} +
+ +
+
+ ); +} diff --git a/src/components/post/poll-display.tsx b/src/components/post/poll-display.tsx new file mode 100644 index 0000000..274ada5 --- /dev/null +++ b/src/components/post/poll-display.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { PollWithOptions } from "@/lib/types"; + +interface PollDisplayProps { + poll: PollWithOptions; + onVote: (optionId: string) => void; + isVoting: boolean; +} + +function formatTimeRemaining(expiresAt: string): string { + const diff = new Date(expiresAt).getTime() - Date.now(); + if (diff <= 0) return "Finalizada"; + + const hours = Math.floor(diff / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + + if (hours >= 24) { + const days = Math.floor(hours / 24); + return `${days}d restantes`; + } + if (hours > 0) return `${hours}h ${minutes}m restantes`; + return `${minutes}m restantes`; +} + +export function PollDisplay({ poll, onVote, isVoting }: PollDisplayProps) { + const hasVoted = poll.user_vote_option_id !== null; + const showResults = hasVoted || poll.is_expired; + + return ( +
e.stopPropagation()}> + {poll.options.map((option) => { + const percentage = + poll.total_votes > 0 ? Math.round((option.vote_count / poll.total_votes) * 100) : 0; + const isUserChoice = option.id === poll.user_vote_option_id; + + if (showResults) { + return ( +
+
+
+ + {isUserChoice && } + {option.label} + + {percentage}% +
+
+ ); + } + + return ( + + ); + })} + +

+ {poll.total_votes} {poll.total_votes === 1 ? "voto" : "votos"} ·{" "} + {formatTimeRemaining(poll.expires_at)} +

+
+ ); +} diff --git a/src/components/post/post-card.tsx b/src/components/post/post-card.tsx index b568fd8..7fb2e0b 100644 --- a/src/components/post/post-card.tsx +++ b/src/components/post/post-card.tsx @@ -25,8 +25,10 @@ import { Textarea } from "@/components/ui/textarea"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { PostActions } from "./post-actions"; import { PostContent } from "./post-content"; +import { PollDisplay } from "./poll-display"; import { useAuth } from "@/hooks/use-auth"; import { useDeletePost, useUpdatePost } from "@/hooks/use-posts"; +import { useVotePoll } from "@/hooks/use-polls"; import { formatRelativeTime, getInitials, cn } from "@/lib/utils"; import { toast } from "sonner"; import type { PostWithCounts } from "@/lib/types"; @@ -48,6 +50,7 @@ export function PostCard({ const { user, isAdmin } = useAuth(); const deletePost = useDeletePost(); const updatePost = useUpdatePost(); + const votePoll = useVotePoll(); const [editOpen, setEditOpen] = useState(false); const [editContent, setEditContent] = useState(post.content); const [lightboxOpen, setLightboxOpen] = useState(false); @@ -191,6 +194,14 @@ export function PostCard({ + {post.poll && ( + votePoll.mutate({ pollId: post.poll!.id, optionId })} + isVoting={votePoll.isPending} + /> + )} + {post.media_urls.length > 0 && (
([]); const [mediaPreviews, setMediaPreviews] = useState([]); const [uploading, setUploading] = useState(false); + const [pollData, setPollData] = useState<{ options: string[]; duration: PollDuration } | null>( + null + ); const [mentionQuery, setMentionQuery] = useState(null); const [mentionIndex, setMentionIndex] = useState(0); const fileInputRef = useRef(null); @@ -155,8 +160,10 @@ export function PostComposer({ setMediaPreviews((prev) => prev.filter((_, i) => i !== index)); }; + const pollIsValid = pollData !== null && pollData.options.filter((o) => o.trim()).length >= 2; + const handleSubmit = async () => { - if (!content.trim() && mediaFiles.length === 0) return; + if (!content.trim() && mediaFiles.length === 0 && !pollIsValid) return; try { let mediaUrls: string[] = []; @@ -181,13 +188,18 @@ export function PostComposer({ setUploading(false); } + const pollPayload = pollIsValid + ? { options: pollData.options.filter((o) => o.trim()), duration: pollData.duration } + : undefined; + createPost.mutate( - { content: content.trim(), mediaUrls, parentId }, + { content: content.trim(), mediaUrls, parentId, poll: pollPayload }, { onSuccess: () => { setContent(""); setMediaFiles([]); setMediaPreviews([]); + setPollData(null); toast.success(parentId ? "Respuesta publicada" : "Publicación creada"); onSuccess?.(); }, @@ -284,6 +296,15 @@ export function PostComposer({
)} + {pollData && ( + setPollData(null)} + /> + )} +
fileInputRef.current?.click()} - disabled={mediaFiles.length >= 4} + disabled={mediaFiles.length >= 4 || pollData !== null} > +
@@ -311,7 +346,9 @@ export function PostComposer({