Retrieve post comments
This commit is contained in:
86
client/src/components/CommentInputModule.tsx
Normal file
86
client/src/components/CommentInputModule.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Formik, Form, FormikHelpers } from "formik";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import config from '../config';
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import NextUIFormikTextarea from "./NextUIFormikTextarea";
|
||||||
|
import instance from '../security/http';
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { retrieveUserInformation } from "../security/users";
|
||||||
|
import { Button } from "@nextui-org/react";
|
||||||
|
import { PaperAirplaneIcon } from "../icons";
|
||||||
|
|
||||||
|
|
||||||
|
const validationSchema = Yup.object({
|
||||||
|
content: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.min(3, "Content must be at least 3 characters")
|
||||||
|
.max(500, "Content must be at most 500 characters")
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function CommentInputModule() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [userId, setUserId] = useState(null);
|
||||||
|
const { id } = useParams(); // Retrieve 'id' from the route
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
content: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const postId = id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getUserInformation = async () => {
|
||||||
|
try {
|
||||||
|
const user = await retrieveUserInformation(); // Get the user ID
|
||||||
|
setUserId(user.id); // Set the user ID in the state
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getUserInformation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submitComment = async (values: any, { resetForm }: FormikHelpers<{ content: string }>) => {
|
||||||
|
const response = await instance.post(config.serverAddress + `/post/${postId}/comments`,
|
||||||
|
{ ...values, userId, postId }
|
||||||
|
);
|
||||||
|
console.log("Comment created succesfully", response.data);
|
||||||
|
resetForm(); // Reset the form after successful submission
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full">
|
||||||
|
<div className="w-2/12"></div>
|
||||||
|
<div className="w-8/12 mx-auto mt-5">
|
||||||
|
<div>
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={submitComment}
|
||||||
|
>
|
||||||
|
{({ isValid, dirty }) => (
|
||||||
|
<Form>
|
||||||
|
<div className="relative">
|
||||||
|
<NextUIFormikTextarea
|
||||||
|
label="Comment"
|
||||||
|
name="content"
|
||||||
|
placeholder="Write your comment here"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end my-2">
|
||||||
|
<Button isIconOnly
|
||||||
|
type="submit"
|
||||||
|
disabled={!isValid || !dirty}
|
||||||
|
className="bg-primary-950 text-white">
|
||||||
|
<PaperAirplaneIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-2/12"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,85 +1,118 @@
|
|||||||
import { Formik, Form } from "formik";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import config from '../config';
|
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import NextUIFormikTextarea from "../components/NextUIFormikTextarea";
|
import instance from "../security/http";
|
||||||
import instance from '../security/http';
|
import config from "../config";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { retrieveUserInformation } from "../security/users";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@nextui-org/react";
|
import { Avatar, Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from "@nextui-org/react";
|
||||||
import { PaperAirplaneIcon } from "../icons";
|
import { ChatBubbleOvalLeftEllipsisIcon, EllipsisHorizontalIcon, HandThumbsUpIcon } from "../icons";
|
||||||
|
|
||||||
|
interface Comment {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
// type User = {
|
||||||
content: Yup.string()
|
// id: string;
|
||||||
.trim()
|
// firstName: string;
|
||||||
.min(3, "Content must be at least 3 characters")
|
// lastName: string;
|
||||||
.max(500, "Content must be at most 500 characters")
|
// };
|
||||||
});
|
|
||||||
|
|
||||||
export default function CommentsModule() {
|
export default function CommentsModule() {
|
||||||
const navigate = useNavigate();
|
const { id } = useParams();
|
||||||
const [userId, setUserId] = useState(null);
|
const [commentList, setCommentList] = useState<Comment[]>([]);
|
||||||
const { id } = useParams(); // Retrieve 'id' from the route
|
|
||||||
|
let postId = id
|
||||||
const initialValues = {
|
|
||||||
content: "",
|
const getComments = async () => {
|
||||||
|
instance
|
||||||
|
.get(config.serverAddress + `/post/${postId}/getComments`).then((res) => {
|
||||||
|
setCommentList(res.data);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const postId = id;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getUserInformation = async () => {
|
getComments();
|
||||||
try {
|
}, [id]);
|
||||||
const user = await retrieveUserInformation(); // Get the user ID
|
|
||||||
setUserId(user.id); // Set the user ID in the state
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
getUserInformation();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const submitComment = async (values: any) => {
|
|
||||||
const response = await instance.post(config.serverAddress + `/post/${postId}/comments`,
|
|
||||||
{ ...values, userId, postId }
|
|
||||||
);
|
|
||||||
console.log("Comment created succesfully", response.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full">
|
<>
|
||||||
<div className="w-2/12"></div>
|
<div className="flex w-full">
|
||||||
<div className="w-full mx-auto mt-5">
|
<div className="w-2/12"></div>
|
||||||
<div>
|
<div className="w-8/12 ml-5 mb-2 text-md font-bold opacity-65">Comment Section</div>
|
||||||
<Formik
|
<div className="w-2/12"></div>
|
||||||
initialValues={initialValues}
|
|
||||||
validationSchema={validationSchema}
|
|
||||||
onSubmit={submitComment}
|
|
||||||
>
|
|
||||||
{({ isValid, dirty }) => (
|
|
||||||
<Form>
|
|
||||||
<div className="relative">
|
|
||||||
<NextUIFormikTextarea
|
|
||||||
label="Comment"
|
|
||||||
name="content"
|
|
||||||
placeholder="Write your comment here"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-end my-2">
|
|
||||||
<Button isIconOnly
|
|
||||||
type="submit"
|
|
||||||
disabled={!isValid || !dirty}
|
|
||||||
className="bg-primary-950 text-white">
|
|
||||||
<PaperAirplaneIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/12"></div>
|
<div className="flex w-full h-full">
|
||||||
</div>
|
<div className="w-2/12"></div>
|
||||||
|
<div className="w-8/12 mx-auto">
|
||||||
|
{commentList.length > 0 ? (
|
||||||
|
commentList.map((comment) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full bg-primary-50 dark:bg-primary-950 rounded-xl mb-2 p-3 mx-auto"
|
||||||
|
key={comment.id}>
|
||||||
|
<div className="flex flex-row w-full">
|
||||||
|
<div>
|
||||||
|
<Avatar
|
||||||
|
src="https://pbs.twimg.com/media/GOva9x5a0AAK8Bn?format=jpg&name=large"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-11/12 text-sm ml-3">
|
||||||
|
<div className="font-bold">Name</div>
|
||||||
|
<div className="break-words whitespace-pre-wrap">{comment.content}</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<div className="flex flex-row-reverse justify-center items-center size-7">
|
||||||
|
<Dropdown>
|
||||||
|
<div>
|
||||||
|
<DropdownTrigger
|
||||||
|
className="justify-center items-center"
|
||||||
|
>
|
||||||
|
<Button isIconOnly variant="light">
|
||||||
|
<EllipsisHorizontalIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu aria-label="Static Actions">
|
||||||
|
<DropdownItem
|
||||||
|
key="edit"
|
||||||
|
textValue="Edit"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key="delete"
|
||||||
|
textValue="Delete"
|
||||||
|
className="text-danger"
|
||||||
|
color="danger"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row ml-14 mt-2 gap-3 w-11/12">
|
||||||
|
<Button variant="light" isIconOnly>
|
||||||
|
<HandThumbsUpIcon />
|
||||||
|
</Button>
|
||||||
|
<Button variant="light" isIconOnly className="w-20">
|
||||||
|
<div>
|
||||||
|
<ChatBubbleOvalLeftEllipsisIcon />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Reply
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full justify-center m-auto p-20 text-gray-500">No comments... It's so lonely 🥹</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-2/12"></div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
@@ -617,18 +617,17 @@ export const TrophyIcon = () => {
|
|||||||
|
|
||||||
export const PaperAirplaneIcon = () => {
|
export const PaperAirplaneIcon = () => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none" viewBox="0 0 24 24"
|
fill="none" viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
strokeWidth={1.5}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className="size-6"
|
className="size-6">
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
strokeLinecap="round"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
|
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
|
||||||
/>
|
</svg>
|
||||||
</svg>
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { retrieveUserInformationById } from "../security/usersbyid";
|
import { retrieveUserInformationById } from "../security/usersbyid";
|
||||||
import { retrieveUserInformation } from "../security/users";
|
import { retrieveUserInformation } from "../security/users";
|
||||||
import { number } from "yup";
|
|
||||||
// import UserPostImage from "../components/UserPostImage";
|
// import UserPostImage from "../components/UserPostImage";
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import instance from "../security/http";
|
import instance from "../security/http";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
ArrowUTurnLeftIcon,
|
ArrowUTurnLeftIcon,
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
import { retrieveUserInformationById } from "../security/usersbyid";
|
import { retrieveUserInformationById } from "../security/usersbyid";
|
||||||
|
import CommentInputModule from "../components/CommentInputModule";
|
||||||
import CommentsModule from "../components/CommentsModule";
|
import CommentsModule from "../components/CommentsModule";
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
@@ -114,7 +115,7 @@ const PostPage: React.FC = () => {
|
|||||||
<ArrowUTurnLeftIcon />
|
<ArrowUTurnLeftIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row w-full gap-4 mx-auto ">
|
<div className="flex flex-row w-8/12 gap-4 mx-auto">
|
||||||
<div className="flex flex-col gap-8 w-full">
|
<div className="flex flex-col gap-8 w-full">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<section
|
<section
|
||||||
@@ -136,7 +137,8 @@ const PostPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row-reverse justify-center items-center">
|
<div className="flex flex-row-reverse justify-center items-center">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger className="justify-center items-center">
|
<DropdownTrigger
|
||||||
|
className="justify-center items-center">
|
||||||
<Button isIconOnly variant="light">
|
<Button isIconOnly variant="light">
|
||||||
<EllipsisHorizontalIcon />
|
<EllipsisHorizontalIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -146,7 +148,7 @@ const PostPage: React.FC = () => {
|
|||||||
key="edit"
|
key="edit"
|
||||||
textValue="Edit"
|
textValue="Edit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`edit/${post.id}`);
|
navigate(`../edit/${post.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
@@ -201,10 +203,13 @@ const PostPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-2/12"></div>
|
<div className="w-2/12"></div>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<CommentInputModule />
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<CommentsModule />
|
<CommentsModule />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
|
|||||||
@@ -45,32 +45,6 @@ router.post("/", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/:id/comments", async (req, res) => {
|
|
||||||
let data = req.body;
|
|
||||||
|
|
||||||
// Validate request body
|
|
||||||
let validationSchema = yup.object({
|
|
||||||
content: yup.string().trim().min(3).max(500).required(),
|
|
||||||
userId: yup.string().required(),
|
|
||||||
postId: yup.string().required()
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Validating schema...");
|
|
||||||
data = await validationSchema.validate(data, { abortEarly: false });
|
|
||||||
|
|
||||||
console.log("Creating comment...");
|
|
||||||
let result = await Comment.create(data);
|
|
||||||
|
|
||||||
res.json(result);
|
|
||||||
console.log("Success!");
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Error caught! Info: " + err);
|
|
||||||
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
|
// // sequelize method findAll is used to generate a standard SELECT query which will retrieve all entries from the table
|
||||||
// router.get("/", async (req, res) => {
|
// router.get("/", async (req, res) => {
|
||||||
// let list = await Tutorial.findAll({
|
// let list = await Tutorial.findAll({
|
||||||
@@ -180,4 +154,46 @@ router.delete("/:id", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("/:id/comments", async (req, res) => {
|
||||||
|
let data = req.body;
|
||||||
|
|
||||||
|
// Validate request body
|
||||||
|
let validationSchema = yup.object({
|
||||||
|
content: yup.string().trim().min(3).max(500).required(),
|
||||||
|
userId: yup.string().required(),
|
||||||
|
postId: yup.string().required()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Validating schema...");
|
||||||
|
data = await validationSchema.validate(data, { abortEarly: false });
|
||||||
|
|
||||||
|
console.log("Creating comment...");
|
||||||
|
let result = await Comment.create(data);
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
console.log("Success!");
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error caught! Info: " + err);
|
||||||
|
res.status(400).json({ errors: err.errors });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/:id/getComments", async (req, res) => {
|
||||||
|
let id = req.params.id;
|
||||||
|
|
||||||
|
let condition = {
|
||||||
|
where: { postId: id },
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check id not found
|
||||||
|
let comments = await Comment.findAll(condition);
|
||||||
|
if (!comments) {
|
||||||
|
res.sendStatus(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(comments);
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user