import React, { useState, useEffect } from "react"; import { useNavigate, Link, useParams } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import { forumAPI, addressAPI } from "../services/api"; import { uploadFiles, getPublicImageUrl } from "../services/uploadService"; import TagInput from "../components/TagInput"; import ForumImageUpload from "../components/ForumImageUpload"; import { Address, ForumPost } from "../types"; const CreateForumPost: React.FC = () => { const { id } = useParams<{ id: string }>(); const isEditMode = !!id; const { user } = useAuth(); const navigate = useNavigate(); const [loading, setLoading] = useState(isEditMode); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [userAddresses, setUserAddresses] = useState([]); const [existingImageKeys, setExistingImageKeys] = useState([]); const [formData, setFormData] = useState({ title: "", content: "", category: "general_discussion" as | "item_request" | "technical_support" | "community_resources" | "general_discussion", tags: [] as string[], zipCode: user?.zipCode || "", latitude: undefined as number | undefined, longitude: undefined as number | undefined, }); const [imageFiles, setImageFiles] = useState([]); const [imagePreviews, setImagePreviews] = useState([]); useEffect(() => { fetchUserAddresses(); if (isEditMode && id) { fetchPost(); } }, [id, isEditMode]); const fetchPost = async () => { try { setLoading(true); const response = await forumAPI.getPost(id!); const post: ForumPost = response.data; // Verify user is the author if (post.authorId !== user?.id) { setError("You are not authorized to edit this post"); setLoading(false); return; } // Populate form with existing data setFormData({ title: post.title, content: post.content, category: post.category as any, tags: (post.tags || []).map((t) => t.tagName), zipCode: post.zipCode || user?.zipCode || "", latitude: post.latitude, longitude: post.longitude, }); // Set existing images if (post.imageFilenames && post.imageFilenames.length > 0) { setExistingImageKeys(post.imageFilenames); setImagePreviews( post.imageFilenames.map((key: string) => getPublicImageUrl(key)) ); } } catch (err: any) { setError(err.response?.data?.error || "Failed to fetch post"); } finally { setLoading(false); } }; const fetchUserAddresses = async () => { try { const response = await addressAPI.getAddresses(); setUserAddresses(response.data); } catch (error) { console.error("Error fetching addresses:", error); } }; const categories = [ { value: "item_request", label: "Item Request", description: "Looking to rent a specific item", }, { value: "technical_support", label: "Technical Support", description: "Get help with using the platform", }, { value: "community_resources", label: "Community Resources", description: "Share tips, guides, and resources", }, { value: "general_discussion", label: "General Discussion", description: "Open-ended conversations", }, ]; const handleInputChange = ( e: React.ChangeEvent< HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement > ) => { const { name, value } = e.target; // If category is being changed to item_request and user has addresses, autopopulate location data if (name === "category" && value === "item_request" && userAddresses.length > 0) { // Try to find primary address first, otherwise use first address const primaryAddress = userAddresses.find(addr => addr.isPrimary) || userAddresses[0]; setFormData((prev) => ({ ...prev, [name]: value, zipCode: primaryAddress.zipCode, latitude: primaryAddress.latitude, longitude: primaryAddress.longitude })); } else { setFormData((prev) => ({ ...prev, [name]: value })); } }; const handleTagsChange = (tags: string[]) => { setFormData((prev) => ({ ...prev, tags })); }; const handleImageChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); const totalImages = existingImageKeys.length + imageFiles.length; const remainingSlots = 5 - totalImages; const filesToAdd = files.slice(0, remainingSlots); setImageFiles((prev) => [...prev, ...filesToAdd]); // Create preview URLs filesToAdd.forEach((file) => { const reader = new FileReader(); reader.onloadend = () => { setImagePreviews((prev) => [...prev, reader.result as string]); }; reader.readAsDataURL(file); }); // Reset input e.target.value = ""; }; const handleRemoveImage = (index: number) => { if (index < existingImageKeys.length) { // Removing an existing S3 image setExistingImageKeys((prev) => prev.filter((_, i) => i !== index)); } else { // Removing a new upload const newFileIndex = index - existingImageKeys.length; setImageFiles((prev) => prev.filter((_, i) => i !== newFileIndex)); } setImagePreviews((prev) => prev.filter((_, i) => i !== index)); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); // Validation - character minimums shown inline, just prevent submission if (!formData.title.trim() || formData.title.length < 10) { return; } if (!formData.content.trim() || formData.content.length < 20) { return; } if (formData.category === "item_request" && !formData.zipCode.trim()) { setError("Zip code is required for item requests"); return; } try { setIsSubmitting(true); // Upload images to S3 first (if any) let imageFilenames: string[] = []; if (imageFiles.length > 0) { const uploadResults = await uploadFiles("forum", imageFiles); imageFilenames = uploadResults.map((result) => result.key); } // Build the post data const postData: { title: string; content: string; category: string; tags?: string[]; zipCode?: string; latitude?: number; longitude?: number; imageFilenames?: string[]; } = { title: formData.title, content: formData.content, category: formData.category, }; // Add tags if present if (formData.tags.length > 0) { postData.tags = formData.tags; } // Add location data for item requests if (formData.category === 'item_request' && formData.zipCode) { postData.zipCode = formData.zipCode; // If we have coordinates from a saved address, send them to avoid re-geocoding if (formData.latitude !== undefined && formData.longitude !== undefined) { postData.latitude = formData.latitude; postData.longitude = formData.longitude; } } // Combine existing and new S3 image keys const allImageKeys = [...existingImageKeys, ...imageFilenames]; if (allImageKeys.length > 0) { postData.imageFilenames = allImageKeys; } if (isEditMode) { await forumAPI.updatePost(id!, postData); navigate(`/forum/${id}`); } else { const response = await forumAPI.createPost(postData); navigate(`/forum/${response.data.id}`); } } catch (err: any) { setError(err.response?.data?.error || err.message || `Failed to ${isEditMode ? 'update' : 'create'} post`); setIsSubmitting(false); } }; if (loading) { return (
Loading...
); } if (!user) { return (
You must be logged in to {isEditMode ? 'edit' : 'create'} a post.
Back to Forum
); } if (error && error.includes("authorized")) { return (
{error}
Back to Forum
); } return (
{/* Guidelines Card - only show for new posts */} {!isEditMode && (
Community Guidelines
  • Be respectful and courteous to others
  • Stay on topic and relevant to the category
  • No spam, advertising, or self-promotion
  • Search before posting to avoid duplicates
  • Use clear, descriptive titles
  • Provide helpful and constructive feedback
)}

{isEditMode ? 'Edit Post' : 'Create New Post'}

{error && (
{error}
)}
{/* Title */}
0 && formData.title.length < 10 ? 'is-invalid' : ''}`} id="title" name="title" value={formData.title} onChange={handleInputChange} placeholder="Enter a descriptive title..." maxLength={200} disabled={isSubmitting} required /> {formData.title.length > 0 && formData.title.length < 10 ? (
{10 - formData.title.length} more characters needed (minimum 10)
) : (
{formData.title.length}/200 characters (minimum 10)
)}
{/* Category */}
Choose the category that best fits your post
{/* Location fields for item requests */} {formData.category === "item_request" && (
Your zip code helps notify nearby users who might have the item you're looking for
)} {/* Content */}