From 40a9e0ffd8a142bbae98a1ab24fa6b3333793c01 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 27 Sep 2025 23:31:43 +0000 Subject: [PATCH] Add PostComposer component for creating text/image/video posts cgen-997bda7ced2149e8bcfea54dcc15f3ee --- client/components/social/PostComposer.tsx | 135 ++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 client/components/social/PostComposer.tsx diff --git a/client/components/social/PostComposer.tsx b/client/components/social/PostComposer.tsx new file mode 100644 index 00000000..4afbb46c --- /dev/null +++ b/client/components/social/PostComposer.tsx @@ -0,0 +1,135 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { useAuth } from "@/contexts/AuthContext"; +import { communityService } from "@/lib/supabase-service"; +import { storage } from "@/lib/supabase"; +import { useToast } from "@/hooks/use-toast"; + +function readFileAsDataURL(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result)); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(file); + }); +} + +export default function PostComposer({ onPosted }: { onPosted?: () => void }) { + const { user } = useAuth(); + const { toast } = useToast(); + const [text, setText] = useState(""); + const [mediaFile, setMediaFile] = useState(null); + const [mediaUrlInput, setMediaUrlInput] = useState(""); + const [submitting, setSubmitting] = useState(false); + + const reset = () => { + setText(""); + setMediaFile(null); + setMediaUrlInput(""); + }; + + const uploadToStorage = async (file: File): Promise => { + try { + if (!storage || !("from" in storage)) return null; + const bucket = storage.from("post_media"); + const ext = file.name.split(".").pop() || "bin"; + const path = `${user?.id || "anon"}/${Date.now()}.${ext}`; + const { data, error } = await bucket.upload(path, file, { + cacheControl: "3600", + upsert: true, + contentType: file.type, + }); + if (error) return null; + if (!data) return null; + const { data: publicUrl } = bucket.getPublicUrl(data.path); + return publicUrl.publicUrl || null; + } catch { + return null; + } + }; + + const handlePost = async () => { + if (!user) { + toast({ title: "Sign in required", description: "Please sign in to post" }); + return; + } + if (!text.trim() && !mediaFile && !mediaUrlInput.trim()) { + toast({ description: "Write something or attach media" }); + return; + } + setSubmitting(true); + try { + let mediaUrl: string | null = mediaUrlInput.trim() || null; + let mediaType: "image" | "video" | "none" = "none"; + + if (mediaFile) { + const maybeUrl = await uploadToStorage(mediaFile); + mediaUrl = maybeUrl; + if (!mediaUrl) { + // fallback to base64 data URL + mediaUrl = await readFileAsDataURL(mediaFile); + } + } + + if (mediaUrl) { + if (/\.(mp4|webm|mov)(\?.*)?$/i.test(mediaUrl) || /video\//.test(mediaFile?.type || "")) { + mediaType = "video"; + } else if (/\.(png|jpe?g|gif|webp|svg)(\?.*)?$/i.test(mediaUrl) || /image\//.test(mediaFile?.type || "")) { + mediaType = "image"; + } + } + + const content = JSON.stringify({ text: text.trim(), mediaUrl, mediaType }); + const title = text.trim().slice(0, 80) || (mediaType === "video" ? "New video" : mediaType === "image" ? "New photo" : "Update"); + + await communityService.createPost({ + author_id: user.id, + title, + content, + category: mediaType === "none" ? "text" : mediaType, + tags: mediaType === "none" ? ["update"] : [mediaType, "feed"], + is_published: true, + } as any); + + toast({ title: "Posted", description: "Your update is live" }); + reset(); + onPosted?.(); + } catch (e: any) { + toast({ variant: "destructive", title: "Could not post", description: e?.message || "Try again later" }); + } finally { + setSubmitting(false); + } + }; + + return ( + + +