Skip to content

Commit e5eb425

Browse files
Feat: Project management system (#1132)
* create /user-written projects route * setup: manage projects dashboard * create /get-written-count of projects * integrate: frontend to fetch contributor projects * render all published projects with stats * render all drafted projects * create /delete project route * typo fixes * functional delete btn * setup projects-pagination system * setup user redirection on projects * debug: update repo & live urls
1 parent e978b03 commit e5eb425

File tree

11 files changed

+400
-10
lines changed

11 files changed

+400
-10
lines changed

backend/Controllers/project.controller.js

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const createProject = async (req, res) => {
3939
let project_id = id || title.replace(/[^a-zA-Z0-9]/g, ' ').replace(/\s+/g, '-').trim() + nanoid();
4040

4141
if (id) {
42-
Project.findOneAndUpdate({ project_id }, { title, des, banner, content, tags, draft: draft ? draft : false })
42+
Project.findOneAndUpdate({ project_id }, { title, des, banner, projectUrl, repository, content, tags, draft: draft ? draft : false })
4343
.then(project => {
4444
return res.status(200).json({ id: project_id });
4545
})
@@ -203,4 +203,73 @@ export const getProject = async (req, res) => {
203203
.catch(err => {
204204
return res.status(500).json({ error: err.message });
205205
})
206+
}
207+
208+
export const userWrittenProjects = async (req, res) => {
209+
210+
let user_id = req.user;
211+
212+
let { page, draft, query, deletedDocCount } = req.body;
213+
214+
let maxLimit = 5;
215+
let skipDocs = (page - 1) * maxLimit;
216+
217+
if (deletedDocCount) {
218+
skipDocs -= deletedDocCount;
219+
}
220+
221+
Project.find({ author: user_id, draft, title: new RegExp(query, 'i') })
222+
.skip(skipDocs)
223+
.limit(maxLimit)
224+
.sort({ publishedAt: -1 })
225+
.select("title banner publishedAt project_id activity des draft -_id")
226+
.then(projects => {
227+
return res.status(200).json({ projects });
228+
})
229+
.catch(err => {
230+
return res.status(500).json({ error: err.message });
231+
})
232+
}
233+
234+
export const userWrittenProjectsCount = async (req, res) => {
235+
236+
let user_id = req.user;
237+
238+
let { draft, query } = req.body;
239+
240+
Project.countDocuments({ author: user_id, draft, title: new RegExp(query, 'i') })
241+
.then(count => {
242+
return res.status(200).json({ totalDocs: count });
243+
})
244+
.catch(err => {
245+
return res.status(500).json({ error: err.message });
246+
})
247+
}
248+
249+
export const deleteProject = async (req, res) => {
250+
251+
let user_id = req.user;
252+
253+
let { project_id } = req.body;
254+
255+
Project.findOneAndDelete({ project_id })
256+
.then(project => {
257+
258+
Notification.deleteMany({ project: project._id })
259+
.then(data => console.log("Nofiication deleted"));
260+
261+
Comment.deleteMany({ project: project._id })
262+
.then(data => console.log("Comments deleted"));
263+
264+
User.findOneAndUpdate({ _id: user_id }, { $pull: { project: project._id }, $inc: { "account_info.total_posts": -1 } })
265+
.then(user => {
266+
return res.status(200).json({ message: "Project deleted successfully" });
267+
})
268+
.catch(err => {
269+
return res.status(500).json({ error: err.message });
270+
})
271+
})
272+
.catch(err => {
273+
return res.status(500).json({ error: err.message });
274+
})
206275
}

backend/Routes/api/project.routes.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import express from "express";
2-
import { allLatestProjectsCount, createProject, getProject, getAllProjects, searchProjects, searchProjectsCount, trendingProjects } from "../../Controllers/project.controller.js";
2+
import { allLatestProjectsCount, createProject, getProject, getAllProjects, searchProjects, searchProjectsCount, trendingProjects, userWrittenProjects, userWrittenProjectsCount, deleteProject } from "../../Controllers/project.controller.js";
33
import { authenticateUser } from "../../Middlewares/auth.middleware.js";
44

55
const projectRoutes = express.Router();
@@ -11,5 +11,8 @@ projectRoutes.post("/search", searchProjects);
1111
projectRoutes.post("/all-latest-count", allLatestProjectsCount);
1212
projectRoutes.post("/search-count", searchProjectsCount);
1313
projectRoutes.post("/get", getProject);
14+
projectRoutes.post("/user-written", authenticateUser, userWrittenProjects);
15+
projectRoutes.post("/user-written-count", authenticateUser, userWrittenProjectsCount);
16+
projectRoutes.post("/delete", authenticateUser, deleteProject);
1417

1518
export default projectRoutes;

frontend/src/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import SideNav from "./components/SideNavBar";
1313
import ChangePassword from "./pages/ChangePassword";
1414
import EditProfile from "./pages/EditProfile";
1515
import Notifications from "./pages/Notifications";
16+
import ManageProjects from "./pages/ManageProjects";
1617

1718
export const UserContext = createContext({});
1819

@@ -33,6 +34,7 @@ function App() {
3334
<Route path="/" element={<Navbar />}>
3435
<Route index element={<Home />} />
3536
<Route path="dashboard" element={<SideNav />}>
37+
<Route path="projects" element={<ManageProjects />} />
3638
<Route path="notifications" element={<Notifications />} />
3739
</Route>
3840
<Route path="settings" element={<SideNav />}>

frontend/src/components/CommentCard.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const CommentCard = ({ index, leftVal, commentData }) => {
1010

1111
let { commented_by: { personal_info: { profile_img, fullname, username: commented_by_username } }, commentedAt, comment, _id, children } = commentData;
1212

13-
let { project, project: { comments, comments: { results: commentsArr }, activity, activity: { total_parent_comments }, author: { personal_info: { username: blog_author } } }, setProject, setTotalParentCommentsLoaded } = useContext(ProjectContext);
13+
let { project, project: { comments, comments: { results: commentsArr }, activity, activity: { total_parent_comments }, author: { personal_info: { username: project_author } } }, setProject, setTotalParentCommentsLoaded } = useContext(ProjectContext);
1414

1515
let { userAuth: { access_token, username } } = useContext(UserContext);
1616

@@ -174,7 +174,7 @@ const CommentCard = ({ index, leftVal, commentData }) => {
174174
<button className="underline" onClick={handleReplyClick}>Reply</button>
175175

176176
{
177-
username === commented_by_username || username === blog_author ?
177+
username === commented_by_username || username === project_author ?
178178
<button
179179
onClick={deleteComment}
180180
className="p-2 px-3 rounded-md border border-gray-100 ml-auto hover:bg-red-50 hover:text-red-500 flex items-center"
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Link } from "react-router-dom";
2+
import { getDay } from "../common/date";
3+
import { useContext, useState } from "react";
4+
import { UserContext } from "../App";
5+
import axios from "axios";
6+
7+
const ProjectStats = ({ stats }) => {
8+
9+
return (
10+
<div className="flex gap-2 max-lg:mb-6 max-lg:pb-6 border-gray-100 max-lg:border-b">
11+
{
12+
Object.keys(stats).map((key, i) => {
13+
return !key.includes("parent") ? (
14+
<div
15+
key={i}
16+
className={"flex flex-col items-center w-full h-full justify-center p-4 px-6 " + (i !== 0 ? " border-gray-100 border-l" : "")}
17+
>
18+
<h1 className="text-xl lg:text-2xl mb-2">
19+
{stats[key].toLocaleString()}
20+
</h1>
21+
<p className="max-lg:text-gray-500 capitalize">
22+
{key.split("_")[1]}
23+
</p>
24+
</div>
25+
) : ""
26+
})
27+
}
28+
</div>
29+
)
30+
}
31+
32+
const deleteProject = (project, access_token, target) => {
33+
34+
let { index, project_id, setStateFunc } = project;
35+
36+
target.setAttribute("disabled", true);
37+
38+
axios.post(import.meta.env.VITE_SERVER_DOMAIN + "/api/project/delete", { project_id }, {
39+
headers: {
40+
Authorization: `Bearer ${access_token}`
41+
}
42+
})
43+
.then(({ data }) => {
44+
45+
target.removeAttribute("disabled");
46+
47+
setStateFunc(preVal => {
48+
let { deletedDocCount, totalDocs, results } = preVal;
49+
50+
results.splice(index, 1);
51+
52+
if (!deletedDocCount) {
53+
deletedDocCount = 0;
54+
}
55+
56+
if (!results.length && totalDocs - 1 > 0) {
57+
return null;
58+
}
59+
60+
return { ...preVal, totalDocs: totalDocs - 1, deletedDocCount: deletedDocCount + 1 };
61+
})
62+
})
63+
.catch(err => {
64+
target.removeAttribute("disabled");
65+
console.log(err);
66+
})
67+
}
68+
69+
export const ManagePublishedProjectCard = ({ project }) => {
70+
71+
let { banner, project_id, title, publishedAt, activity } = project;
72+
73+
let { userAuth: { access_token } } = useContext(UserContext);
74+
75+
let [showStat, setShowStat] = useState(false);
76+
77+
return (
78+
<>
79+
<div className="flex gap-10 border-b mb-6 max-md:px-4 border-gray-100 pb-6 items-center">
80+
<img src={banner} alt="" className="max-md:hidden lg:hidden xl:block w-28 h-28 flex-none bg-gray-100 object-cover" />
81+
82+
<div className="flex flex-col justify-between py-2 w-full min-w-[300px]">
83+
<div>
84+
<Link to={`/project/${project_id}`} className="project-title mb-4 hover:underline">{title}</Link>
85+
86+
<p className="line-clamp-1">Published on {getDay(publishedAt)}</p>
87+
</div>
88+
89+
<div className="flex gap-6 mt-3">
90+
<Link to={`/editor/${project_id}`} className="pr-4 py-2 underline">Edit</Link>
91+
92+
<button
93+
className="lg:hidden pr-4 py-2 underline"
94+
onClick={() => setShowStat(preVal => !preVal)}
95+
>
96+
Stats
97+
</button>
98+
99+
<button
100+
className="pr-4 py-2 underline text-red-500"
101+
onClick={(e) => deleteProject(project, access_token, e.target)}
102+
>
103+
Delete
104+
</button>
105+
106+
</div>
107+
</div>
108+
109+
<div className="max-lg:hidden">
110+
<ProjectStats stats={activity} />
111+
</div>
112+
</div>
113+
114+
{
115+
showStat ?
116+
<div className="lg:hidden">
117+
<ProjectStats stats={activity} />
118+
</div>
119+
: ""
120+
}
121+
</>
122+
)
123+
}
124+
125+
export const ManageDraftProjectPost = ({ project }) => {
126+
127+
let { title, des, project_id, index } = project;
128+
129+
let { userAuth: { access_token } } = useContext(UserContext);
130+
131+
index++;
132+
133+
return (
134+
<div className="flex gap-5 lg:gap-10 pb-6 border-b mb-6 border-gray-100">
135+
<h1 className="project-index text-center ph-4 md:pl-6 flex-none">
136+
{index < 10 ? "0" + index : index}
137+
</h1>
138+
139+
<div>
140+
<h1 className="project-title mb-3">{title}</h1>
141+
<p className="line-clamp-2 font-gelasio">{des.length ? des : "No Description"}</p>
142+
143+
<div className="flex gap-6 mt-3">
144+
<Link to={`/editor/${project_id}`} className="pr-4 py-2 underline">Edit</Link>
145+
146+
<button
147+
className="pr-4 py-2 underline text-red-500"
148+
onClick={(e) => deleteProject(project, access_token, e.target)}
149+
>
150+
Delete
151+
</button>
152+
</div>
153+
</div>
154+
</div>
155+
)
156+
}

frontend/src/components/ProjectEditor.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ const ProjectEditor = () => {
139139
toast.success("Project saved successfully");
140140

141141
setTimeout(() => {
142-
navigate("/");
142+
navigate("/dashboard/projects?tab=draft");
143143
}, 500);
144144
})
145145
.catch(({ response }) => {

frontend/src/components/PublishForm.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const PublishForm = () => {
9797
toast.success("Project published successfully");
9898

9999
setTimeout(() => {
100-
navigate("/");
100+
navigate("/dashboard/projects");
101101
}, 500);
102102
})
103103
.catch(({ response }) => {

frontend/src/components/UserNavigationPanel.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const UserNavigationPanel = () => {
2727
Profile
2828
</Link>
2929

30-
<Link to="/dahboard/projects" className="link pl-8 py-4">
30+
<Link to="/dashboard/projects" className="link pl-8 py-4">
3131
Dashboard
3232
</Link>
3333

frontend/src/index.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
}
114114

115115
.project-index {
116-
@apply text-4xl sm:text-3xl lg:text-5xl font-bold text-gray-600 leading-none;
116+
@apply text-4xl sm:text-3xl lg:text-5xl font-bold text-gray-300 leading-none;
117117
}
118118
}
119119

0 commit comments

Comments
 (0)