Skip to content

Commit 95cee86

Browse files
authored
Merge pull request #2 from VirtusLab-Open-Source/feat/comments-nextjs-example
2 parents 7d28c12 + cca0577 commit 95cee86

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+14339
-0
lines changed

strapi-nextjs-comments/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Strapi Plugin Comments example with NextJS
2+
3+
This example is set up to show how to create a project with our comments plugin installed as well as integrate it with a sample frontend.
4+
5+
## 🔧 Getting Started
6+
7+
To run this project, you need to prepare two shell windows. One window will be used to set up and run strapi server and the second one will be used to run development server for nextjs frontend.
8+
9+
### Strapi Server
10+
11+
Install all packages
12+
13+
```sh
14+
cd ./strapi-app
15+
yarn install
16+
```
17+
18+
Run the server
19+
20+
> Before running the strapi server be sure to create your own `.env` file. Example of this file can be found in strapi app folder.
21+
22+
```sh
23+
yarn build
24+
yarn develop
25+
26+
# or
27+
28+
yarn develop --watch-admin
29+
```
30+
31+
After that open strapi admin panel and create admin user. The strapi project should be ready to use at this point.
32+
33+
### NextJS development server
34+
35+
Install all packages
36+
```sh
37+
cd ./next-app
38+
yarn install
39+
```
40+
41+
Run the server
42+
```sh
43+
yarn dev
44+
```
45+
46+
After that the nextJs frontend should be ready to use. You can direct to `localhost:3000` to see it.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Author, Report } from "../types/comments";
2+
3+
export async function PostComment(postId: number, author: Author, content: string) {
4+
const body = JSON.stringify({ author, content });
5+
const res = await fetch(`http://localhost:1337/api/comments/api::post.post:${postId}`,
6+
{
7+
headers: {
8+
'Accept': 'application/json',
9+
'Content-Type': 'application/json'
10+
},
11+
method: "POST",
12+
body,
13+
}
14+
);
15+
}
16+
17+
export async function ReportComment(postId: number, commentId: number, report: Report) {
18+
const body = JSON.stringify(report);
19+
const res = await fetch(`http://localhost:1337/api/comments/api::post.post:${postId}/comment/${commentId}/report-abuse`,
20+
{
21+
headers: {
22+
'Accept': 'application/json',
23+
'Content-Type': 'application/json'
24+
},
25+
method: "POST",
26+
body
27+
}
28+
);
29+
}
30+
31+
export async function GetComments(postId: number) {
32+
const res = await fetch(`http://localhost:1337/api/comments/api::post.post:${postId}`);
33+
const data = await res.json();
34+
return data;
35+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Dropdown } from "react-bootstrap"
2+
import { Comment, Report } from "../../types/comments"
3+
4+
interface IProps {
5+
comment: Comment;
6+
postId: number;
7+
reportComment: (postId: number, commentId: number, report: Report) => Promise<void>;
8+
}
9+
10+
const reports: Array<Report & { label: string }> = [
11+
{ reason: "BAD_WORDS", content: "Comment reported for containing bad words", label: "Report for bad words" },
12+
{ reason: "DISCRIMINATION", content: "Comment reported for being discriminative", label: "Report for discrimination" },
13+
{ reason: "OTHER", content: "Comment reported for unspecified reason", label: "Report for other" },
14+
];
15+
16+
const Comment: React.FC<IProps> = ({ comment, postId, reportComment }) => {
17+
const blocked = comment.blocked;
18+
return (
19+
<div className="list-group-item d-flex gap-3 py-3 " key={comment.id} aria-hidden="true">
20+
<img src="https://github.com/twbs.png" alt="twbs" width="32" height="32" className="rounded-circle flex-shrink-0" />
21+
<div className="d-flex gap-2 w-100 justify-content-between">
22+
<div>
23+
<h6 className="mb-0">{!blocked ? comment.author.name : "Comment Blocked"}</h6>
24+
<p className={`mb-0 opacity-75 ${blocked ? "placeholder" : ""}`}>{comment.content}</p>
25+
</div>
26+
<div className="d-flex align-items-center">
27+
{!blocked && <Dropdown>
28+
<Dropdown.Toggle variant="success" id="dropdown-basic" size="sm">
29+
Report
30+
</Dropdown.Toggle>
31+
<Dropdown.Menu>
32+
{reports.map(report => (
33+
<Dropdown.Item
34+
key={report.reason}
35+
onClick={() => reportComment(postId, comment.id, report)}>
36+
{report.label}
37+
</Dropdown.Item>
38+
))}
39+
</Dropdown.Menu>
40+
</Dropdown>}
41+
<small className={`opacity-50 text-nowrap mx-2 ${blocked ? "placeholder" : ""}`}>{comment.createdAt}</small>
42+
</div>
43+
</div>
44+
</div>
45+
)
46+
};
47+
48+
export default Comment;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useFormik } from "formik";
2+
import { Author } from "../../types/comments";
3+
4+
interface IProps {
5+
postId: number;
6+
postComment: (postId: number, author: Author, content: string, threadOf?: number) => Promise<void>;
7+
}
8+
9+
const NewCommentForm: React.FC<IProps> = ({ postId, postComment }) => {
10+
const formik = useFormik({
11+
initialValues: {
12+
name: "",
13+
email: "",
14+
content: "",
15+
},
16+
onSubmit: async () => {
17+
const author = {
18+
id: formik.values.email.replace("@", "_"),
19+
name: formik.values.name,
20+
email: formik.values.email,
21+
}
22+
await postComment(postId, author, formik.values.content);
23+
formik.setValues(formik.initialValues);
24+
}
25+
});
26+
27+
const defaultProps = (field: "name" | "email" | "content") => ({
28+
className: "form-control",
29+
value: formik.values[field],
30+
onChange: formik.handleChange,
31+
required: true,
32+
disabled: formik.isSubmitting,
33+
id: field,
34+
name: field,
35+
});
36+
37+
return (
38+
<form className="mt-5" onSubmit={formik.handleSubmit}>
39+
<h4>
40+
Add new comment
41+
</h4>
42+
<div className="gap-2 w-75 row">
43+
<div className="col-3">
44+
<label className="form-label" htmlFor="name">Name</label>
45+
<input {...defaultProps("name")} type="text" placeholder="Your display name" />
46+
<div className="invalid-feedback">
47+
Valid last name is required.
48+
</div>
49+
</div>
50+
<div className="col-4">
51+
<label className="form-label" htmlFor="email">Email</label>
52+
<input {...defaultProps("email")} type="email" placeholder="Your email" />
53+
<div className="invalid-feedback">
54+
Valid last name is required.
55+
</div>
56+
</div>
57+
<div className="row">
58+
<div className="col-12">
59+
<label className="form-label" htmlFor="content">Content</label>
60+
<input {...defaultProps("content")} type="textarea" placeholder="Content of your comment"/>
61+
</div>
62+
</div>
63+
<div className="row m-1">
64+
<button className="form-control btn btn-primary btn-md w-25" type="submit">Add new comment</button>
65+
</div>
66+
</div>
67+
</form>
68+
);
69+
}
70+
71+
export default NewCommentForm;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import usePostComments from "../../hooks/useComments";
2+
import Comment from "./Comment";
3+
import NewCommentForm from "./NewCommentForm";
4+
5+
interface IProps {
6+
postId: number;
7+
}
8+
9+
const CommentsList: React.FC<IProps> = ({ postId }) => {
10+
const { data, isLoading, reportComment, postComment } = usePostComments(postId);
11+
12+
if (isLoading || !data) {
13+
return <div>Loading comments...</div>;
14+
}
15+
16+
return (
17+
<>
18+
<div className="list-group w-auto">
19+
<h2 className="blog-post-title mt-4">Comments</h2>
20+
<hr />
21+
{
22+
data.map(comment =>
23+
<Comment
24+
key={`comment_${comment.id}`}
25+
postId={postId}
26+
comment={comment}
27+
reportComment={reportComment}
28+
/>)
29+
}
30+
</div>
31+
<NewCommentForm postId={postId} postComment={postComment} />
32+
</>
33+
);
34+
}
35+
36+
export default CommentsList;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Link from "next/link";
2+
3+
const footerItems = [{
4+
id: 1,
5+
title: 'Strapi Documentation',
6+
path: 'https://docs.strapi.io/',
7+
}, {
8+
id: 2,
9+
title: 'Strapi Plugin Comments',
10+
path: 'https://github.com/VirtusLab-Open-Source/strapi-plugin-comments/',
11+
}, {
12+
id: 3,
13+
title: 'Strapi Plugin Navigation',
14+
path: 'https://github.com/VirtusLab-Open-Source/strapi-plugin-navigation/',
15+
}];
16+
17+
const MainFooter: React.FC = () => {
18+
return (
19+
<footer className="footer mt-auto py-3 bg-light">
20+
<div className="container d-flex flex-wrap justify-content-between align-items-center">
21+
<p className="col-md-4 mb-0 text-muted">Created by VirtusLab</p>
22+
<ul className="nav col-md-auto justify-content-end">
23+
{footerItems.map((navItem) => (
24+
<Link key={navItem.id} href={navItem.path} passHref>
25+
<li className="nav-item nav-link px-3 text-muted">
26+
{navItem.title}
27+
</li>
28+
</Link>
29+
))}
30+
</ul>
31+
</div>
32+
</footer>
33+
);
34+
}
35+
36+
export default MainFooter;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Container } from "react-bootstrap";
2+
import Footer from "../Footer";
3+
import Navbar from "../Navbar";
4+
5+
interface IProps {
6+
children: JSX.Element
7+
}
8+
9+
const MainLayout: React.FC<IProps> = ({ children }) => {
10+
return (
11+
<div className="d-flex flex-column" style={{ height: "100vh" }}>
12+
<Navbar />
13+
<Container className="my-5">
14+
{children}
15+
</Container>
16+
<Footer />
17+
</div>
18+
);
19+
}
20+
21+
export default MainLayout;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Container, Navbar } from 'react-bootstrap';
2+
import Link from "next/link";
3+
4+
const MainNavbar: React.FC<{}> = () => {
5+
return (
6+
<Navbar bg="light" expand="lg">
7+
<Container>
8+
<Link href="/">
9+
<Navbar.Brand className="cursor-pointer">
10+
Strapi Plugin Comments Example
11+
</Navbar.Brand>
12+
</Link>
13+
</Container>
14+
</Navbar>
15+
);
16+
}
17+
18+
export default MainNavbar;

0 commit comments

Comments
 (0)