images for forum and forum comments
This commit is contained in:
@@ -1111,7 +1111,7 @@ router.post('/posts/:id/comments', authenticateToken, async (req, res, next) =>
|
||||
// PUT /api/forum/comments/:id - Edit comment
|
||||
router.put('/comments/:id', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const { content } = req.body;
|
||||
const { content, imageFilenames: rawImageFilenames } = req.body;
|
||||
const comment = await ForumComment.findByPk(req.params.id);
|
||||
|
||||
if (!comment) {
|
||||
@@ -1126,7 +1126,19 @@ router.put('/comments/:id', authenticateToken, async (req, res, next) => {
|
||||
return res.status(400).json({ error: 'Cannot edit deleted comment' });
|
||||
}
|
||||
|
||||
await comment.update({ content });
|
||||
const updateData = { content };
|
||||
|
||||
// Handle image filenames if provided
|
||||
if (rawImageFilenames !== undefined) {
|
||||
const imageFilenamesArray = Array.isArray(rawImageFilenames) ? rawImageFilenames : [];
|
||||
const keyValidation = validateS3Keys(imageFilenamesArray, 'forum', { maxKeys: IMAGE_LIMITS.forum });
|
||||
if (!keyValidation.valid) {
|
||||
return res.status(400).json({ error: keyValidation.error });
|
||||
}
|
||||
updateData.imageFilenames = imageFilenamesArray;
|
||||
}
|
||||
|
||||
await comment.update(updateData);
|
||||
|
||||
const updatedComment = await ForumComment.findByPk(comment.id, {
|
||||
include: [
|
||||
|
||||
@@ -159,6 +159,14 @@ const AppContent: React.FC = () => {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/forum/:id/edit"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<CreateForumPost />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/my-posts"
|
||||
element={
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import ForumImageUpload from './ForumImageUpload';
|
||||
import { IMAGE_LIMITS } from '../config/imageLimits';
|
||||
|
||||
interface CommentFormProps {
|
||||
onSubmit: (content: string, images: File[]) => Promise<void>;
|
||||
@@ -24,7 +25,7 @@ const CommentForm: React.FC<CommentFormProps> = ({
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
const remainingSlots = 3 - imageFiles.length;
|
||||
const remainingSlots = IMAGE_LIMITS.forum - imageFiles.length;
|
||||
const filesToAdd = files.slice(0, remainingSlots);
|
||||
|
||||
setImageFiles((prev) => [...prev, ...filesToAdd]);
|
||||
@@ -81,15 +82,13 @@ const CommentForm: React.FC<CommentFormProps> = ({
|
||||
/>
|
||||
{error && <div className="invalid-feedback">{error}</div>}
|
||||
</div>
|
||||
{!isReply && (
|
||||
<ForumImageUpload
|
||||
imageFiles={imageFiles}
|
||||
imagePreviews={imagePreviews}
|
||||
onImageChange={handleImageChange}
|
||||
onRemoveImage={handleRemoveImage}
|
||||
compact={true}
|
||||
/>
|
||||
)}
|
||||
<ForumImageUpload
|
||||
imageFiles={imageFiles}
|
||||
imagePreviews={imagePreviews}
|
||||
onImageChange={handleImageChange}
|
||||
onRemoveImage={handleRemoveImage}
|
||||
compact={true}
|
||||
/>
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { ForumComment } from "../types";
|
||||
import CommentForm from "./CommentForm";
|
||||
import ForumImageUpload from "./ForumImageUpload";
|
||||
import { getPublicImageUrl } from "../services/uploadService";
|
||||
import { IMAGE_LIMITS } from "../config/imageLimits";
|
||||
|
||||
interface CommentThreadProps {
|
||||
comment: ForumComment;
|
||||
onReply: (commentId: string, content: string) => Promise<void>;
|
||||
onEdit?: (commentId: string, content: string) => Promise<void>;
|
||||
onReply: (commentId: string, content: string, images?: File[]) => Promise<void>;
|
||||
onEdit?: (commentId: string, content: string, existingImageKeys: string[], newImageFiles: File[]) => Promise<void>;
|
||||
onDelete?: (commentId: string) => Promise<void>;
|
||||
onMarkAsAnswer?: (commentId: string) => Promise<void>;
|
||||
currentUserId?: string;
|
||||
@@ -37,6 +39,11 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
const [editContent, setEditContent] = useState(comment.content);
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
// Image editing state
|
||||
const [existingImageKeys, setExistingImageKeys] = useState<string[]>([]);
|
||||
const [editImageFiles, setEditImageFiles] = useState<File[]>([]);
|
||||
const [editImagePreviews, setEditImagePreviews] = useState<string[]>([]);
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
@@ -59,17 +66,67 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
};
|
||||
|
||||
const handleReply = async (content: string, images: File[]) => {
|
||||
// Replies don't support images, so we ignore the images parameter
|
||||
await onReply(comment.id, content);
|
||||
await onReply(comment.id, content, images);
|
||||
setShowReplyForm(false);
|
||||
};
|
||||
|
||||
const handleEdit = async () => {
|
||||
if (onEdit && editContent.trim() !== comment.content) {
|
||||
await onEdit(comment.id, editContent);
|
||||
setIsEditing(false);
|
||||
const startEditing = () => {
|
||||
setIsEditing(true);
|
||||
setEditContent(comment.content);
|
||||
const existingKeys = comment.imageFilenames || [];
|
||||
setExistingImageKeys(existingKeys);
|
||||
setEditImagePreviews(existingKeys.map((key) => getPublicImageUrl(key)));
|
||||
setEditImageFiles([]);
|
||||
};
|
||||
|
||||
const cancelEditing = () => {
|
||||
setIsEditing(false);
|
||||
setEditContent(comment.content);
|
||||
setExistingImageKeys([]);
|
||||
setEditImageFiles([]);
|
||||
setEditImagePreviews([]);
|
||||
};
|
||||
|
||||
const handleEditImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
const totalImages = existingImageKeys.length + editImageFiles.length;
|
||||
const remainingSlots = IMAGE_LIMITS.forum - totalImages;
|
||||
const filesToAdd = files.slice(0, remainingSlots);
|
||||
|
||||
setEditImageFiles((prev) => [...prev, ...filesToAdd]);
|
||||
|
||||
filesToAdd.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setEditImagePreviews((prev) => [...prev, reader.result as string]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const handleRemoveEditImage = (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;
|
||||
setEditImageFiles((prev) => prev.filter((_, i) => i !== newFileIndex));
|
||||
}
|
||||
setEditImagePreviews((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleEdit = async () => {
|
||||
if (onEdit && editContent.trim()) {
|
||||
await onEdit(comment.id, editContent, existingImageKeys, editImageFiles);
|
||||
setIsEditing(false);
|
||||
setExistingImageKeys([]);
|
||||
setEditImageFiles([]);
|
||||
setEditImagePreviews([]);
|
||||
} else {
|
||||
cancelEditing();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -188,7 +245,14 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
value={editContent}
|
||||
onChange={(e) => setEditContent(e.target.value)}
|
||||
/>
|
||||
<div className="d-flex gap-2">
|
||||
<ForumImageUpload
|
||||
imageFiles={editImageFiles}
|
||||
imagePreviews={editImagePreviews}
|
||||
onImageChange={handleEditImageChange}
|
||||
onRemoveImage={handleRemoveEditImage}
|
||||
compact={true}
|
||||
/>
|
||||
<div className="d-flex gap-2 mt-2">
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={handleEdit}
|
||||
@@ -198,10 +262,7 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditContent(comment.content);
|
||||
}}
|
||||
onClick={cancelEditing}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -222,8 +283,8 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
className="img-fluid rounded"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100px",
|
||||
objectFit: "cover",
|
||||
maxHeight: "300px",
|
||||
objectFit: "contain",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() =>
|
||||
@@ -267,7 +328,7 @@ const CommentThread: React.FC<CommentThreadProps> = ({
|
||||
{isAuthor && onEdit && !isEditing && (
|
||||
<button
|
||||
className="btn btn-sm btn-link text-decoration-none p-0"
|
||||
onClick={() => setIsEditing(true)}
|
||||
onClick={startEditing}
|
||||
>
|
||||
<i className="bi bi-pencil me-1"></i>
|
||||
Edit
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { forumAPI, addressAPI } from "../services/api";
|
||||
import { uploadFiles } from "../services/uploadService";
|
||||
import { uploadFiles, getPublicImageUrl } from "../services/uploadService";
|
||||
import TagInput from "../components/TagInput";
|
||||
import ForumImageUpload from "../components/ForumImageUpload";
|
||||
import { Address } from "../types";
|
||||
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<string | null>(null);
|
||||
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
|
||||
const [existingImageKeys, setExistingImageKeys] = useState<string[]>([]);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
@@ -33,7 +37,48 @@ const CreateForumPost: React.FC = () => {
|
||||
|
||||
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 {
|
||||
@@ -96,7 +141,8 @@ const CreateForumPost: React.FC = () => {
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
const remainingSlots = 5 - imageFiles.length;
|
||||
const totalImages = existingImageKeys.length + imageFiles.length;
|
||||
const remainingSlots = 5 - totalImages;
|
||||
const filesToAdd = files.slice(0, remainingSlots);
|
||||
|
||||
setImageFiles((prev) => [...prev, ...filesToAdd]);
|
||||
@@ -115,7 +161,14 @@ const CreateForumPost: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
setImageFiles((prev) => prev.filter((_, i) => i !== index));
|
||||
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));
|
||||
};
|
||||
|
||||
@@ -123,24 +176,12 @@ const CreateForumPost: React.FC = () => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
// Validation
|
||||
if (!formData.title.trim()) {
|
||||
setError("Title is required");
|
||||
// Validation - character minimums shown inline, just prevent submission
|
||||
if (!formData.title.trim() || formData.title.length < 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.title.length < 10) {
|
||||
setError("Title must be at least 10 characters long");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.content.trim()) {
|
||||
setError("Content is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.content.length < 20) {
|
||||
setError("Content must be at least 20 characters long");
|
||||
if (!formData.content.trim() || formData.content.length < 20) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,25 +231,58 @@ const CreateForumPost: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Add S3 image keys
|
||||
if (imageFilenames.length > 0) {
|
||||
postData.imageFilenames = imageFilenames;
|
||||
// Combine existing and new S3 image keys
|
||||
const allImageKeys = [...existingImageKeys, ...imageFilenames];
|
||||
if (allImageKeys.length > 0) {
|
||||
postData.imageFilenames = allImageKeys;
|
||||
}
|
||||
|
||||
const response = await forumAPI.createPost(postData);
|
||||
navigate(`/forum/${response.data.id}`);
|
||||
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 create post");
|
||||
setError(err.response?.data?.error || err.message || `Failed to ${isEditMode ? 'update' : 'create'} post`);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<i className="bi bi-exclamation-triangle me-2"></i>
|
||||
You must be logged in to create a post.
|
||||
You must be logged in to {isEditMode ? 'edit' : 'create'} a post.
|
||||
</div>
|
||||
<Link to="/forum" className="btn btn-secondary">
|
||||
<i className="bi bi-arrow-left me-2"></i>
|
||||
Back to Forum
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && error.includes("authorized")) {
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<i className="bi bi-exclamation-triangle me-2"></i>
|
||||
{error}
|
||||
</div>
|
||||
<Link to="/forum" className="btn btn-secondary">
|
||||
<i className="bi bi-arrow-left me-2"></i>
|
||||
@@ -225,37 +299,44 @@ const CreateForumPost: React.FC = () => {
|
||||
<li className="breadcrumb-item">
|
||||
<Link to="/forum">Forum</Link>
|
||||
</li>
|
||||
{isEditMode && (
|
||||
<li className="breadcrumb-item">
|
||||
<Link to={`/forum/${id}`}>Post</Link>
|
||||
</li>
|
||||
)}
|
||||
<li className="breadcrumb-item active" aria-current="page">
|
||||
Create Post
|
||||
{isEditMode ? 'Edit' : 'Create Post'}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
{/* Guidelines Card */}
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">
|
||||
<h6 className="mb-0">
|
||||
<i className="bi bi-info-circle me-2"></i>
|
||||
Community Guidelines
|
||||
</h6>
|
||||
{/* Guidelines Card - only show for new posts */}
|
||||
{!isEditMode && (
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">
|
||||
<h6 className="mb-0">
|
||||
<i className="bi bi-info-circle me-2"></i>
|
||||
Community Guidelines
|
||||
</h6>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<ul className="mb-0">
|
||||
<li>Be respectful and courteous to others</li>
|
||||
<li>Stay on topic and relevant to the category</li>
|
||||
<li>No spam, advertising, or self-promotion</li>
|
||||
<li>Search before posting to avoid duplicates</li>
|
||||
<li>Use clear, descriptive titles</li>
|
||||
<li>Provide helpful and constructive feedback</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<ul className="mb-0">
|
||||
<li>Be respectful and courteous to others</li>
|
||||
<li>Stay on topic and relevant to the category</li>
|
||||
<li>No spam, advertising, or self-promotion</li>
|
||||
<li>Search before posting to avoid duplicates</li>
|
||||
<li>Use clear, descriptive titles</li>
|
||||
<li>Provide helpful and constructive feedback</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="mb-0">Create New Post</h3>
|
||||
<h3 className="mb-0">{isEditMode ? 'Edit Post' : 'Create New Post'}</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{error && (
|
||||
@@ -272,7 +353,7 @@ const CreateForumPost: React.FC = () => {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
className={`form-control ${formData.title.length > 0 && formData.title.length < 10 ? 'is-invalid' : ''}`}
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
@@ -282,9 +363,15 @@ const CreateForumPost: React.FC = () => {
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
<div className="form-text">
|
||||
{formData.title.length}/200 characters (minimum 10)
|
||||
</div>
|
||||
{formData.title.length > 0 && formData.title.length < 10 ? (
|
||||
<div className="invalid-feedback d-block">
|
||||
{10 - formData.title.length} more characters needed (minimum 10)
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-text">
|
||||
{formData.title.length}/200 characters (minimum 10)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
@@ -343,7 +430,7 @@ const CreateForumPost: React.FC = () => {
|
||||
Content <span className="text-danger">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
className={`form-control ${formData.content.length > 0 && formData.content.length < 20 ? 'is-invalid' : ''}`}
|
||||
id="content"
|
||||
name="content"
|
||||
rows={10}
|
||||
@@ -353,9 +440,15 @@ const CreateForumPost: React.FC = () => {
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
<div className="form-text">
|
||||
{formData.content.length} characters (minimum 20)
|
||||
</div>
|
||||
{formData.content.length > 0 && formData.content.length < 20 ? (
|
||||
<div className="invalid-feedback d-block">
|
||||
{20 - formData.content.length} more characters needed (minimum 20)
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-text">
|
||||
{formData.content.length} characters (minimum 20)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
@@ -426,17 +519,17 @@ const CreateForumPost: React.FC = () => {
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Creating...
|
||||
{isEditMode ? 'Saving...' : 'Creating...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi bi-send me-2"></i>
|
||||
Create Post
|
||||
<i className={`bi ${isEditMode ? 'bi-check-lg' : 'bi-send'} me-2`}></i>
|
||||
{isEditMode ? 'Save Changes' : 'Create Post'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<Link
|
||||
to="/forum"
|
||||
to={isEditMode ? `/forum/${id}` : '/forum'}
|
||||
className={`btn btn-secondary ${
|
||||
isSubmitting ? "disabled" : ""
|
||||
}`}
|
||||
|
||||
@@ -72,16 +72,24 @@ const ForumPostDetail: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReply = async (parentCommentId: string, content: string) => {
|
||||
const handleReply = async (parentCommentId: string, content: string, images: File[] = []) => {
|
||||
if (!user) {
|
||||
alert('Please log in to reply');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Upload images to S3 first (if any)
|
||||
let imageFilenames: string[] = [];
|
||||
if (images.length > 0) {
|
||||
const uploadResults = await uploadFiles("forum", images);
|
||||
imageFilenames = uploadResults.map((result) => result.key);
|
||||
}
|
||||
|
||||
await forumAPI.createComment(id!, {
|
||||
content,
|
||||
parentId: parentCommentId,
|
||||
imageFilenames: imageFilenames.length > 0 ? imageFilenames : undefined,
|
||||
});
|
||||
await fetchPost(); // Refresh to get new reply
|
||||
} catch (err: any) {
|
||||
@@ -89,9 +97,27 @@ const ForumPostDetail: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditComment = async (commentId: string, content: string) => {
|
||||
const handleEditComment = async (
|
||||
commentId: string,
|
||||
content: string,
|
||||
existingImageKeys: string[],
|
||||
newImageFiles: File[]
|
||||
) => {
|
||||
try {
|
||||
await forumAPI.updateComment(commentId, { content });
|
||||
// Upload new images to S3
|
||||
let newImageFilenames: string[] = [];
|
||||
if (newImageFiles.length > 0) {
|
||||
const uploadResults = await uploadFiles("forum", newImageFiles);
|
||||
newImageFilenames = uploadResults.map((result) => result.key);
|
||||
}
|
||||
|
||||
// Combine existing and new image keys
|
||||
const allImageKeys = [...existingImageKeys, ...newImageFilenames];
|
||||
|
||||
await forumAPI.updateComment(commentId, {
|
||||
content,
|
||||
imageFilenames: allImageKeys,
|
||||
});
|
||||
await fetchPost(); // Refresh to get updated comment
|
||||
} catch (err: any) {
|
||||
throw new Error(err.response?.data?.error || 'Failed to update comment');
|
||||
@@ -354,7 +380,7 @@ const ForumPostDetail: React.FC = () => {
|
||||
src={getPublicImageUrl(image)}
|
||||
alt={`Post image`}
|
||||
className="img-fluid rounded"
|
||||
style={{ width: '100%', height: '200px', objectFit: 'cover', cursor: 'pointer' }}
|
||||
style={{ width: '100%', maxHeight: '400px', objectFit: 'contain', cursor: 'pointer' }}
|
||||
onClick={() => window.open(getPublicImageUrl(image), '_blank')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -266,6 +266,9 @@ export interface ForumPost {
|
||||
isPinned: boolean;
|
||||
acceptedAnswerId?: string;
|
||||
imageFilenames?: string[];
|
||||
zipCode?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
isDeleted?: boolean;
|
||||
deletedBy?: string;
|
||||
deletedAt?: string;
|
||||
|
||||
Reference in New Issue
Block a user