Added CommunityPost Image Function
This commit is contained in:
556
client/pnpm-lock.yaml
generated
556
client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
44
client/src/components/InsertPostImage.tsx
Normal file
44
client/src/components/InsertPostImage.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface InsertPostImageProps {
|
||||
onImageSelected: (file: File | null) => void;
|
||||
}
|
||||
|
||||
const InsertPostImage: React.FC<InsertPostImageProps> = ({ onImageSelected }) => {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [previewImage, setPreviewImage] = useState<string>('');
|
||||
|
||||
const handleImageSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFiles = event.target.files as FileList;
|
||||
const file = selectedFiles?.[0] || null;
|
||||
setSelectedFile(file);
|
||||
setPreviewImage(file ? URL.createObjectURL(file) : '');
|
||||
onImageSelected(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col dark:bg-zinc-800 rounded-md ${selectedFile ? 'h-auto' : 'h-20'}`}
|
||||
style={{ width: 300 }}>
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleImageSelect}
|
||||
className="mb-3"
|
||||
/>
|
||||
</div>
|
||||
{selectedFile && (
|
||||
<div>
|
||||
<img
|
||||
src={previewImage}
|
||||
alt="Selected Image"
|
||||
className="w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InsertPostImage;
|
||||
@@ -542,19 +542,17 @@ export const ArrowTopRightOnSquare = () => {
|
||||
|
||||
export const TrashDeleteIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="size-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6">
|
||||
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -57,6 +57,7 @@ export default function CommunityPage() {
|
||||
const [search, setSearch] = useState(""); // Search Function
|
||||
const [userInformation, setUserInformation] = useState<Record<string, User>>({});
|
||||
const [currentUserInfo, setCurrentUserInfo] = useState(null);
|
||||
const [imageErrorFlags, setImageErrorFlags] = useState<Record<string, boolean>>({});
|
||||
|
||||
let accessToken = localStorage.getItem("accessToken");
|
||||
if (!accessToken) {
|
||||
@@ -80,7 +81,6 @@ export default function CommunityPage() {
|
||||
setCommunityList(res.data);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPosts();
|
||||
}, []);
|
||||
@@ -165,6 +165,7 @@ export default function CommunityPage() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handlePostClick = (id: string) => {
|
||||
navigate(`post/${id}`);
|
||||
};
|
||||
@@ -174,6 +175,13 @@ export default function CommunityPage() {
|
||||
return `${config.serverAddress}/users/profile-image/${userId}`;
|
||||
};
|
||||
|
||||
const handleImageError = (postId: string) => {
|
||||
setImageErrorFlags((prevFlags) => ({
|
||||
...prevFlags,
|
||||
[postId]: true,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<div className="flex flex-row gap-4 m-10">
|
||||
@@ -244,9 +252,13 @@ export default function CommunityPage() {
|
||||
<div>
|
||||
<p>{post.content}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Image</p>
|
||||
</div>
|
||||
{!imageErrorFlags[post.id] && post.postImage && post.postImage !== null && (
|
||||
<div>
|
||||
<img src={`${config.serverAddress}/post/post-image/${post.id}`}
|
||||
className="w-[300px] h-auto rounded-lg object-cover"
|
||||
onError={() => handleImageError(post.id)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
|
||||
@@ -9,6 +9,7 @@ import config from "../config";
|
||||
import { ArrowUTurnLeftIcon } from "../icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { retrieveUserInformation } from "../security/users";
|
||||
import InsertPostImage from "../components/InsertPostImage";
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
title: Yup.string()
|
||||
@@ -29,16 +30,20 @@ const validationSchema = Yup.object({
|
||||
"Only letters, numbers, commas, spaces, exclamation marks, quotations, and common symbols are allowed"
|
||||
)
|
||||
.required("Content is required"),
|
||||
postImage: Yup.mixed(),
|
||||
});
|
||||
|
||||
function CreatePostPage() {
|
||||
const navigate = useNavigate();
|
||||
const [userId, setUserId] = useState(null);
|
||||
// Add state for image selection
|
||||
|
||||
const initialValues = {
|
||||
title: "",
|
||||
content: "",
|
||||
postImage: null,
|
||||
tags: "",
|
||||
userId: "",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,17 +60,30 @@ function CreatePostPage() {
|
||||
|
||||
const handleSubmit = async (
|
||||
values: any,
|
||||
{ setSubmitting, resetForm, setFieldError }: any
|
||||
{ setSubmitting, resetForm, setFieldError, setFieldValue }: any
|
||||
) => {
|
||||
try {
|
||||
const postData = {
|
||||
...values,
|
||||
userId: userId
|
||||
const formData = new FormData();
|
||||
formData.append("title", values.title);
|
||||
formData.append("content", values.content);
|
||||
if (values.postImage) {
|
||||
formData.append("postImage", values.postImage);
|
||||
}
|
||||
const response = await axios.post(config.serverAddress + "/post", postData); // Assuming an API route
|
||||
formData.append("tags", values.tags);
|
||||
formData.append("userId", userId || ""); // Ensure userId is appended to formData
|
||||
|
||||
console.log("Submitting formData:", formData);
|
||||
|
||||
const response = await axios.post(config.serverAddress + "/post", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
console.log("Post created successfully:", response.data);
|
||||
resetForm(); // Clear form after successful submit
|
||||
setFieldValue("postImage", null);
|
||||
navigate(-1);
|
||||
} else {
|
||||
console.error("Error creating post:", response.statusText);
|
||||
@@ -99,7 +117,7 @@ function CreatePostPage() {
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isValid, dirty, isSubmitting }) => (
|
||||
{({ isValid, dirty, isSubmitting, setFieldValue }) => (
|
||||
<Form className="flex flex-col gap-5">
|
||||
<div>
|
||||
<NextUIFormikInput
|
||||
@@ -110,11 +128,6 @@ function CreatePostPage() {
|
||||
labelPlacement="inside"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div>
|
||||
<p>Image</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<NextUIFormikTextarea
|
||||
label="Content"
|
||||
@@ -131,10 +144,19 @@ function CreatePostPage() {
|
||||
labelPlacement="inside"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="flex flex-row gap-10">
|
||||
<InsertPostImage
|
||||
onImageSelected={(file) => {
|
||||
setFieldValue("postImage", file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse">
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-primary-color text-white text-xl w-1/12"
|
||||
className="bg-primary-950 text-white text-xl w-1/12"
|
||||
disabled={!isValid || !dirty || isSubmitting}
|
||||
>
|
||||
<p>Post</p>
|
||||
|
||||
@@ -9,6 +9,7 @@ import NextUIFormikTextarea from "../components/NextUIFormikTextarea";
|
||||
import config from "../config";
|
||||
import instance from "../security/http";
|
||||
import { ArrowUTurnLeftIcon } from "../icons";
|
||||
import InsertPostImage from "../components/InsertPostImage";
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
title: Yup.string()
|
||||
@@ -29,20 +30,26 @@ const validationSchema = Yup.object({
|
||||
"Only letters, numbers, commas, spaces, exclamation marks, quotations, and common symbols are allowed"
|
||||
)
|
||||
.required("Content is required"),
|
||||
postImage: Yup.mixed(),
|
||||
});
|
||||
|
||||
function editPost() {
|
||||
function EditPostPage() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [post, setPost] = useState({
|
||||
title: "",
|
||||
content: "",
|
||||
postImage: null,
|
||||
tags: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
instance.get(config.serverAddress + `/post/${id}`).then((res) => {
|
||||
setPost(res.data);
|
||||
setPost({
|
||||
...res.data,
|
||||
postImage: `${config.serverAddress}/post/post-image/${id}`, // Set image URL
|
||||
});
|
||||
setLoading(false);
|
||||
});
|
||||
}, [id]);
|
||||
@@ -52,13 +59,24 @@ function editPost() {
|
||||
{ setSubmitting, resetForm, setFieldError }: any
|
||||
) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("title", values.title);
|
||||
formData.append("content", values.content);
|
||||
if (values.postImage) {
|
||||
formData.append("postImage", values.postImage);
|
||||
}
|
||||
formData.append("tags", values.tags);
|
||||
|
||||
const response = await instance.put(
|
||||
config.serverAddress + `/post/${id}`,
|
||||
values
|
||||
formData,
|
||||
{ headers: { "Content-Type": "multipart/form-data" } }
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
console.log("Post updated successfully:", response.data);
|
||||
resetForm();
|
||||
// Set a flag to indicate a refresh is needed
|
||||
navigate(-1);
|
||||
} else {
|
||||
console.error("Error updating post:", response.statusText);
|
||||
@@ -93,7 +111,7 @@ function editPost() {
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isValid, dirty, isSubmitting }) => (
|
||||
{({ isValid, dirty, isSubmitting, setFieldValue }) => (
|
||||
<Form className="flex flex-col gap-5">
|
||||
<div>
|
||||
<NextUIFormikInput
|
||||
@@ -104,9 +122,6 @@ function editPost() {
|
||||
labelPlacement="inside"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<p>Image</p>
|
||||
</div>
|
||||
<div>
|
||||
<NextUIFormikTextarea
|
||||
label="Content"
|
||||
@@ -123,10 +138,19 @@ function editPost() {
|
||||
labelPlacement="inside"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="flex flex-row gap-10">
|
||||
<InsertPostImage
|
||||
onImageSelected={(file) => {
|
||||
setFieldValue("postImage", file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse">
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-primary-color text-white text-xl w-1/6"
|
||||
className="bg-primary-950 text-white text-xl w-1/6"
|
||||
disabled={!isValid || !dirty || isSubmitting}
|
||||
>
|
||||
<p>Update</p>
|
||||
@@ -141,4 +165,4 @@ function editPost() {
|
||||
);
|
||||
}
|
||||
|
||||
export default editPost;
|
||||
export default EditPostPage;
|
||||
|
||||
@@ -50,6 +50,7 @@ const PostPage: React.FC = () => {
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
|
||||
const [userInformation, setUserInformation] = useState<Record<string, User>>({});
|
||||
const [imageErrorFlags, setImageErrorFlags] = useState<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
@@ -57,7 +58,7 @@ const PostPage: React.FC = () => {
|
||||
.then((res) => setPost(res.data))
|
||||
.catch((error) => console.error("Error fetching post:", error));
|
||||
}
|
||||
}, [id]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (post) {
|
||||
@@ -112,6 +113,13 @@ const PostPage: React.FC = () => {
|
||||
};
|
||||
const profilePictureUrl = post ? getProfilePicture(post.userId) : "";
|
||||
|
||||
const handleImageError = (postId: string) => {
|
||||
setImageErrorFlags((prevFlags) => ({
|
||||
...prevFlags,
|
||||
[postId]: true,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<section className="flex">
|
||||
@@ -147,8 +155,8 @@ const PostPage: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex flex-row-reverse justify-center items-center">
|
||||
<Dropdown>
|
||||
<DropdownTrigger
|
||||
className="justify-center items-center">
|
||||
<DropdownTrigger
|
||||
className="justify-center items-center">
|
||||
<Button isIconOnly variant="light">
|
||||
<EllipsisHorizontalIcon />
|
||||
</Button>
|
||||
@@ -179,15 +187,14 @@ const PostPage: React.FC = () => {
|
||||
<div>
|
||||
<p>{post.content}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Image</p>
|
||||
{/* {userInformation && (
|
||||
<UserPostImage
|
||||
userId={userInformation}
|
||||
editable={true}
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
{!imageErrorFlags[post.id] && post.postImage && post.postImage !== null && (
|
||||
<div>
|
||||
<img src={`${config.serverAddress}/post/post-image/${post.id}`}
|
||||
alt="PostImage"
|
||||
className="w-[300px] h-auto rounded-lg object-cover"
|
||||
onError={() => handleImageError(post.id)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"mysql2": "^3.10.1",
|
||||
"nodemon": "^3.1.3",
|
||||
"openai": "^4.53.2",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.33.4",
|
||||
"uuid": "^10.0.0",
|
||||
|
||||
2094
server/pnpm-lock.yaml
generated
2094
server/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -16,18 +16,21 @@ filter.addWords(...newBadWords);
|
||||
let removeWords = [''];
|
||||
filter.removeWords(...removeWords);
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
router.post("/", upload.fields([{ name: 'postImage', maxCount: 1 }]), async (req, res) => {
|
||||
let data = req.body;
|
||||
let files = req.files;
|
||||
|
||||
// Validate request body
|
||||
let validationSchema = yup.object({
|
||||
title: yup.string().trim().min(3).max(200).required(), // yup object to define validation schema
|
||||
title: yup.string().trim().min(3).max(200).required(),
|
||||
content: yup.string().trim().min(3).max(500).required(),
|
||||
userId: yup.string().required(),
|
||||
postImage: yup.string().trim().max(255),
|
||||
});
|
||||
try {
|
||||
data = await validationSchema.validate(data, // validate() method is used to validate data against the schema and returns the valid data and any applied transformations
|
||||
{ abortEarly: false }); // abortEarly: false means the validation won’t stop when the first error is detected
|
||||
// Process valid data
|
||||
data = await validationSchema.validate(data, { abortEarly: false });
|
||||
|
||||
// Check for profanity
|
||||
if (filter.isProfane(data.title)) {
|
||||
@@ -37,14 +40,18 @@ router.post("/", async (req, res) => {
|
||||
return res.status(400).json({ field: 'content', error: 'Profane content detected in content' });
|
||||
}
|
||||
|
||||
let result = await Post.create(data); // sequelize method create() is used to insert data into the database table
|
||||
let postImage = files.postImage ? files.postImage[0].buffer : null;
|
||||
|
||||
// Process valid data
|
||||
let result = await Post.create({ ...data, postImage });
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
res.status(400).json({ errors: err.errors }); // If the error is caught, return the bad request
|
||||
res.status(400).json({ errors: err.errors });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// // sequelize method findAll is used to generate a standard SELECT query which will retrieve all entries from the table
|
||||
// router.get("/", async (req, res) => {
|
||||
// let list = await Tutorial.findAll({
|
||||
@@ -85,19 +92,44 @@ router.get("/:id", async (req, res) => {
|
||||
res.json(post);
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
router.get("/post-image/:id", async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let post = await Post.findByPk(id);
|
||||
|
||||
if (!post || !post.postImage) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
res.set("Content-Type", "image/jpeg"); // Adjust the content type as necessary
|
||||
res.send(post.postImage);
|
||||
} catch (err) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "Error retrieving post image", error: err });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id", upload.fields([{ name: 'postImage', maxCount: 1 }]), async (req, res) => {
|
||||
let id = req.params.id;
|
||||
let files = req.files;
|
||||
|
||||
// Check id not found
|
||||
let post = await Post.findByPk(id);
|
||||
if (!post) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
let data = req.body;
|
||||
let postImage = files.postImage ? files.postImage[0].buffer : null;
|
||||
|
||||
// Validate request body
|
||||
let validationSchema = yup.object({
|
||||
title: yup.string().trim().min(3).max(100),
|
||||
content: yup.string().trim().min(3).max(500)
|
||||
content: yup.string().trim().min(3).max(500),
|
||||
postImage: yup.mixed(),
|
||||
});
|
||||
try {
|
||||
data = await validationSchema.validate(data,
|
||||
@@ -111,14 +143,17 @@ router.put("/:id", async (req, res) => {
|
||||
return res.status(400).json({ field: 'content', error: 'Profane content detected in content' });
|
||||
}
|
||||
|
||||
// Include the postImage if present
|
||||
if (postImage) {
|
||||
data.postImage = postImage;
|
||||
}
|
||||
|
||||
// Process valid data
|
||||
let post = await Post.update(data, { // update() updates data based on the where condition, and returns the number of rows affected
|
||||
where: { id: id } // If num equals 1, return OK, otherwise return Bad Request
|
||||
});
|
||||
if (post == 1) {
|
||||
res.json({
|
||||
message: "Post was updated successfully."
|
||||
});
|
||||
if (post) {
|
||||
res.json({ message: "Post was updated successfully." });
|
||||
}
|
||||
else {
|
||||
res.status(400).json({
|
||||
|
||||
Reference in New Issue
Block a user