Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Added managing contact messages from admin successfully issue 426 #431

Merged
merged 2 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion admin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
},
"dependencies": {
"axios": "^1.7.2",
"dompurify": "^3.1.6",
"chart.js": "^4.4.3",
"dompurify": "^3.1.6",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.2.1",
"react-responsive-modal": "^6.4.2",
"react-router-dom": "^6.24.0",
"react-switch": "^7.0.0",
"recoil": "^0.7.7"
Expand Down
9 changes: 9 additions & 0 deletions admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Posts from "./pages/Posts";
import { Toaster } from "react-hot-toast";
import UpdatePost from "./components/UpdatePost";
import Graphs from "./pages/Graphs";
import ContactMessages from "./pages/ContactMessages";
// import axios from "axios";
// axios.defaults.baseURL = "http://localhost:3001/";

Expand Down Expand Up @@ -77,6 +78,14 @@ function App() {
</AuthenticatedRoute>
}
/>
<Route
path="/admin/contactmessages"
element={
<AuthenticatedRoute>
<ContactMessages />
</AuthenticatedRoute>
}
/>
<Route path="*" element={<PageNotFound />} />
</Routes>
<Toaster/>
Expand Down
2 changes: 2 additions & 0 deletions admin/src/components/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IoNewspaperOutline } from "react-icons/io5";
import logo from '../assets/favicon.png';
import { Link, useLocation } from 'react-router-dom';
import { VscGraphScatter } from "react-icons/vsc";
import { MdOutlineAttachEmail } from "react-icons/md";

const SideBar = ({ sidebarOpen, toggleSidebar }: { sidebarOpen: boolean, toggleSidebar: () => void }) => {
const location = useLocation();
Expand All @@ -29,6 +30,7 @@ const SideBar = ({ sidebarOpen, toggleSidebar }: { sidebarOpen: boolean, toggleS
<Link to="/admin/users" className={linkClasses('/admin/users')}><HiOutlineUsers size={23} className='mr-3'/>All Users</Link>
<Link to="/admin/posts" className={linkClasses('/admin/posts')}><IoNewspaperOutline size={23} className='mr-3'/>All Posts</Link>
<Link to="/admin/statistics" className={linkClasses('/admin/statistics')}><VscGraphScatter size={23} className='mr-3'/>Statistics</Link>
<Link to="/admin/contactmessages" className={linkClasses('/admin/contactmessages')}><MdOutlineAttachEmail size={23} className='mr-3'/>Messages</Link>
</nav>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion admin/src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind utilities;
117 changes: 117 additions & 0 deletions admin/src/pages/ContactMessages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useState, useEffect } from "react";
import axios from "axios";
import Navbar from "../components/Navbar";
import SideBar from "../components/SideBar";
import { useRecoilValue } from "recoil";
import { tokenState } from "../store/atoms/auth";
import { IContactMessage } from "../types";
import { Modal } from 'react-responsive-modal';
import 'react-responsive-modal/styles.css';
import '../styles/Model.css'

const ContactMessages = () => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [contactMessages, setContactMessages] = useState<IContactMessage[]>([]);
const [open, setOpen] = useState(false);
const [selectedMessage, setSelectedMessage] = useState<IContactMessage | null>(null);
const token = useRecoilValue(tokenState);

document.title = "Style Share Admin | Manage Contact Messages 👥";

const toggleSidebar = () => {
setSidebarOpen(!sidebarOpen);
};

useEffect(() => {
const fetchMessages = async () => {
try {
const response = await axios.get("/api/v1/admin/geallcontactmessages", {
headers: {
Authorization: `Bearer ${token}`,
},
});
setContactMessages(response.data.contactMessage);
} catch (error) {
console.error("Error fetching messages:", error);
}
};

fetchMessages();
}, [token]);

const handleOpenModal = (message: IContactMessage) => {
setSelectedMessage(message);
setOpen(true);
};

const handleCloseModal = () => {
setOpen(false);
setSelectedMessage(null);
};

return (
<div>
<Navbar toggleSidebar={toggleSidebar} />
<div className="flex-1 flex flex-col lg:ml-80">
<SideBar sidebarOpen={sidebarOpen} toggleSidebar={toggleSidebar} />
<div className="mx-5 lg:mr-11 overflow-x-auto shadow-md rounded-xl mb-5">
<table className="w-full rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-xs md:text-sm text-white uppercase bg-sky-500 text-center">
<tr>
<th scope="col" className="px-8 py-3 text-start">Name</th>
<th scope="col" className="px-6 py-3">Date</th>
<th scope="col" className="px-6 py-3">Subject</th>
<th scope="col" className="px-6 py-3">Message</th>
<th scope="col" className="px-6 py-3">Action</th>
</tr>
</thead>
<tbody>
{contactMessages.map(contactMessage => (
<tr key={contactMessage.id} className="text-xs md:text-sm text-center border-b bg-[#000435] border-sky-500 hover:bg-blue-950 hover:text-white">
<td className="px-8 py-4 font-semibold">
<div className="flex flex-col items-start">
<span className="font-bold">{contactMessage.name}</span>
<span className="font-thin text-gray-300">{contactMessage.email}</span>
</div>
</td>
<td className="px-8 py-4 font-semibold">{new Date(contactMessage.createdAt).toLocaleDateString()}</td>
<td className="px-8 py-4 font-semibold">{contactMessage.subject.slice(0, 10)}</td>
<td className="px-12 py-4 font-semibold">{contactMessage.message.slice(0, 10)}</td>
<td className="px-2 py-4 grid grid-cols-1 gap-3 justify-center md:grid-cols-2">
<button
onClick={() => handleOpenModal(contactMessage)}
className="font-semibold rounded-md p-2 bg-sky-500 text-white border-2 hover:bg-sky-600"
>
Read
</button>
<a
href={`https://mail.google.com/mail/?view=cm&fs=1&to=${contactMessage.email}`}
target="_blank"
rel="noopener noreferrer"
className="font-semibold rounded-md p-2 bg-yellow-500 text-white border-2 hover:bg-yellow-600"
>
Reply
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<Modal open={open} onClose={handleCloseModal} center classNames={{ modal: 'customModal',overlay: 'customOverlay'}}>
{selectedMessage && (
<div className="p-6">
<h2 className="text-xl font-bold text-gray-800 mb-4">Subject: {selectedMessage.subject}</h2>
<p className="text-gray-700 mb-4 text-justify">{selectedMessage.message}</p>
<p className="text-sm text-gray-600 mb-2"><strong>From:</strong> {selectedMessage.name}</p>
<p className="text-sm text-gray-600 mb-2"><strong>Email:</strong> {selectedMessage.email}</p>
<p className="text-sm text-gray-600"><strong>Delivered on:</strong> {new Date(selectedMessage.createdAt).toLocaleString()}</p>
</div>
)}
</Modal>
</div>
);
};

export default ContactMessages;
35 changes: 14 additions & 21 deletions admin/src/pages/Posts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import { useRecoilValue } from "recoil";
import { tokenState } from "../store/atoms/auth";
import toast from "react-hot-toast";
import { Link } from "react-router-dom";
import { RiEditCircleFill } from "react-icons/ri";
import { IoMdTrash } from "react-icons/io";

const Posts = () => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [posts, setPosts] = useState<IPost[]>([]);
Expand Down Expand Up @@ -62,38 +59,34 @@ const Posts = () => {
<thead className="text-xs text-white uppercase bg-sky-500">
<tr>
<th scope="col" className="px-8 py-3">Title</th>
<th scope="col" className="px-6 py-3">Author</th>
<th scope="col" className="px-8 py-3">Author</th>
<th scope="col" className="px-3 py-3">createdAt</th>
<th scope="col" className="px-6 py-3">Likes</th>
<th scope="col" className="px-4 py-3">Likes</th>
<th scope="col" className="px-6 py-3">Comments</th>
<th scope="col" className="px-16 py-3">Action</th>
</tr>
</thead>
<tbody>
{posts.map(post => (
<tr key={post.id} className="border-b bg-[#000435] border-sky-500 hover:bg-blue-950 hover:text-white">
<tr key={post.id} className="text-xs md:text-sm border-b bg-[#000435] border-sky-500 hover:bg-blue-950 hover:text-white">
<td className="px-8 font-semibold text-white">{post.title}</td>
<td className="flex items-center px-6 py-5 text-white">
<div>
<div className="text-base font-bold">{post.author.username}</div>
<div className="font-medium text-gray-300 ">{post.author.email}</div>
<td className="px-8 py-4 font-semibold">
<div className="flex flex-col items-start">
<span className="font-bold">{post.author.username}</span>
<span className="font-thin text-gray-300">{post.author.email}</span>
</div>
</td>
<td className="px-3 font-semibold">{new Date(post.createdAt).toLocaleDateString()}</td>
<td className="px-8 font-semibold">{post.reactions.length}</td>
<td className="px-12 font-semibold">{post.comments.length}</td>
<td className="flex flex-col px-9">
<div className="flex gap-2">
<Link to={`/admin/update-post/${post.id}`}>
<button className="font-semibold rounded-md py-2 mt-5 px-3 bg-sky-500 text-white hover:bg-sky-600 flex items-center">
<RiEditCircleFill size={20} className="mr-1 " />
</button>
<td className="px-2 py-4 grid grid-cols-1 gap-3 justify-center md:grid-cols-2 text-center">
<Link to={`/admin/update-post/${post.id}`} className="font-semibold rounded-md p-2 bg-sky-500 text-white border-2 hover:bg-sky-600">
Update
</Link>
<button onClick={() => handleDelete(post.id)} className="font-semibold mt-5 rounded-md py-2 px-3 bg-sky-500 text-white hover:bg-sky-600 flex items-center">
<IoMdTrash size={20} className="mr-1" />
</button>
</div>
</td>
<button onClick={() => handleDelete(post.id)} className="font-semibold rounded-md p-2 bg-red-500 text-white border-2 hover:bg-red-600">
Delete
</button>
</td>
</tr>
))}
</tbody>
Expand Down
6 changes: 6 additions & 0 deletions admin/src/styles/Model.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.customOverlay {
background: rgba(149, 188, 205, 0.174);
}
.customModal {
border-radius: 15px;
}
11 changes: 10 additions & 1 deletion admin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,13 @@ export interface IPost {
totalReactions: number;
contactMessages: number;
favoritesPosts: number;
}
}

export interface IContactMessage{
id:string;
name:string,
email:string,
subject:string,
message:string,
createdAt:number
}
23 changes: 23 additions & 0 deletions backend/src/routes/admin/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,27 @@ export const deletePostController = async (req: UserAuthRequest, res: Response)
error: "An unexpected exception occurred!",
});
}
};

export const getAllContactMessages = async (req: Request, res: Response) => {
try{
const contactMessage = await prisma.contactMessage.findMany({
select: {
id: true,
name:true,
email:true,
subject:true,
message:true,
createdAt:true
},
});
res.status(200).json({
message: "Successfully fetched contact messages!",
contactMessage,
});
}catch(error){
res.status(500).json({
error: "An unexpected exception occurred!",
});
}
};
4 changes: 3 additions & 1 deletion backend/src/routes/admin/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Router} from 'express';
import { adminLoginController, adminProfileController, allUserForAdmin, blockUserController, unblockUserController, getAdminPostsController, getAdminTrendingPostsController, getAdminStatsController, getGraphsStatsController, updatePostController, deletePostController, getPostByIdController } from './controller';
import { adminLoginController, adminProfileController, allUserForAdmin, blockUserController, unblockUserController, getAdminPostsController, getAdminTrendingPostsController, getAdminStatsController, getGraphsStatsController, updatePostController, deletePostController, getPostByIdController, getAllContactMessages } from './controller';
import { isAdmin } from '../../middleware/adminAuth';

const adminRouter = Router();
Expand Down Expand Up @@ -28,4 +28,6 @@ adminRouter.patch('/posts/update/:postId', isAdmin, updatePostController);

adminRouter.delete('/posts/delete/:postId', isAdmin, deletePostController);

adminRouter.get("/geallcontactmessages", isAdmin,getAllContactMessages);

export default adminRouter;
Loading