From 85b97e078d1cac3098535634a630404740577b22 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Thu, 13 Nov 2025 06:33:50 +0000 Subject: [PATCH] Create PostComposer component for creating/editing community posts cgen-7c7567ada8e84596a8d57b233756abc8 --- client/components/feed/PostComposer.tsx | 369 ++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 client/components/feed/PostComposer.tsx diff --git a/client/components/feed/PostComposer.tsx b/client/components/feed/PostComposer.tsx new file mode 100644 index 00000000..d44f4140 --- /dev/null +++ b/client/components/feed/PostComposer.tsx @@ -0,0 +1,369 @@ +import { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { aethexToast } from "@/lib/aethex-toast"; +import ArmPostCard, { ArmType } from "@/components/feed/ArmPostCard"; +import { Loader2, X, Tag } from "lucide-react"; + +const API_BASE = import.meta.env.VITE_API_BASE || ""; + +const ARM_OPTIONS: { id: ArmType; label: string }[] = [ + { id: "labs", label: "Labs" }, + { id: "gameforge", label: "GameForge" }, + { id: "corp", label: "Corp" }, + { id: "foundation", label: "Foundation" }, + { id: "devlink", label: "Dev-Link" }, + { id: "nexus", label: "Nexus" }, + { id: "staff", label: "Staff" }, +]; + +const CATEGORIES = [ + "Announcement", + "Tutorial", + "Update", + "Question", + "Discussion", + "Showcase", + "Resource", + "Other", +]; + +interface Post { + id?: string; + title: string; + content: string; + arm_affiliation: ArmType; + tags?: string[]; + category?: string; + user_profiles?: { + id: string; + username?: string; + full_name?: string; + avatar_url?: string; + }; + created_at?: string; +} + +interface PostComposerProps { + open: boolean; + onOpenChange: (open: boolean) => void; + currentUserId?: string; + currentUserProfile?: { + id: string; + username?: string; + full_name?: string; + avatar_url?: string; + }; + editingPost?: Post; + onSuccess?: () => void; +} + +export default function PostComposer({ + open, + onOpenChange, + currentUserId, + currentUserProfile, + editingPost, + onSuccess, +}: PostComposerProps) { + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [armAffiliation, setArmAffiliation] = useState("labs"); + const [category, setCategory] = useState(""); + const [tagInput, setTagInput] = useState(""); + const [tags, setTags] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Initialize form with editing post data + useEffect(() => { + if (editingPost) { + setTitle(editingPost.title); + setContent(editingPost.content); + setArmAffiliation(editingPost.arm_affiliation); + setCategory(editingPost.category || ""); + setTags(editingPost.tags || []); + } else { + resetForm(); + } + }, [editingPost, open]); + + const resetForm = () => { + setTitle(""); + setContent(""); + setArmAffiliation("labs"); + setCategory(""); + setTagInput(""); + setTags([]); + }; + + const handleAddTag = (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault(); + const trimmedTag = tagInput.trim().toLowerCase(); + if (trimmedTag && !tags.includes(trimmedTag) && tags.length < 5) { + setTags([...tags, trimmedTag]); + setTagInput(""); + } + } + }; + + const handleRemoveTag = (tagToRemove: string) => { + setTags(tags.filter((tag) => tag !== tagToRemove)); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!currentUserId) { + aethexToast.error({ + title: "Not authenticated", + description: "Please log in to create posts", + }); + return; + } + + if (!title.trim()) { + aethexToast.error({ + title: "Empty title", + description: "Please enter a title", + }); + return; + } + + if (!content.trim()) { + aethexToast.error({ + title: "Empty content", + description: "Please write some content", + }); + return; + } + + setIsSubmitting(true); + try { + const url = `${API_BASE}/api/community/posts`; + const method = editingPost ? "PUT" : "POST"; + + const payload = { + ...(editingPost && { id: editingPost.id }), + title: title.trim(), + content: content.trim(), + arm_affiliation: armAffiliation, + author_id: currentUserId, + category: category || null, + tags: tags, + ...(editingPost && { user_id: currentUserId }), + }; + + const response = await fetch(url, { + method, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (response.ok) { + const data = await response.json(); + aethexToast.success({ + title: editingPost ? "Post updated" : "Post created", + description: editingPost + ? "Your post has been updated" + : "Your post has been published to the community feed", + }); + resetForm(); + onOpenChange(false); + onSuccess?.(); + } else { + const error = await response.json(); + aethexToast.error({ + title: editingPost ? "Failed to update post" : "Failed to create post", + description: error.error || "Please try again", + }); + } + } catch (error) { + console.error("Error submitting post:", error); + aethexToast.error({ + title: editingPost ? "Failed to update post" : "Failed to create post", + description: "An unexpected error occurred", + }); + } finally { + setIsSubmitting(false); + } + }; + + const previewPost: Post = { + title: title || "Untitled Post", + content: content || "Start typing your post content...", + arm_affiliation: armAffiliation, + tags, + category: category || undefined, + user_profiles: currentUserProfile, + created_at: new Date().toISOString(), + }; + + return ( + + + + + {editingPost ? "Edit Post" : "Create a Community Post"} + + + +
+ {/* Form Section */} +
+ {/* Title */} +
+ + setTitle(e.target.value)} + maxLength={500} + disabled={isSubmitting} + className="text-base" + /> +

+ {title.length}/500 +

+
+ + {/* Content */} +
+ +