diff --git a/package-lock.json b/package-lock.json
index 5c35d07..5831e0e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,7 @@
"react-hot-toast": "^2.4.1",
"react-icons": "^4.8.0",
"react-markdown": "^9.0.0",
- "react-router-dom": "^6.14.2",
+ "react-router-dom": "^6.21.1",
"react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0",
"simplemde": "^1.11.2",
@@ -3616,11 +3616,11 @@
}
},
"node_modules/@remix-run/router": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz",
- "integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.1.tgz",
+ "integrity": "sha512-Qg4DMQsfPNAs88rb2xkdk03N3bjK4jgX5fR24eHCTR9q6PrhZQZ4UJBPzCHJkIpTRN1UKxx2DzjZmnC+7Lj0Ow==",
"engines": {
- "node": ">=14"
+ "node": ">=14.0.0"
}
},
"node_modules/@rollup/plugin-babel": {
@@ -15475,29 +15475,29 @@
}
},
"node_modules/react-router": {
- "version": "6.14.2",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.2.tgz",
- "integrity": "sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==",
+ "version": "6.21.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.1.tgz",
+ "integrity": "sha512-W0l13YlMTm1YrpVIOpjCADJqEUpz1vm+CMo47RuFX4Ftegwm6KOYsL5G3eiE52jnJpKvzm6uB/vTKTPKM8dmkA==",
"dependencies": {
- "@remix-run/router": "1.7.2"
+ "@remix-run/router": "1.14.1"
},
"engines": {
- "node": ">=14"
+ "node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
- "version": "6.14.2",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz",
- "integrity": "sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==",
+ "version": "6.21.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.1.tgz",
+ "integrity": "sha512-QCNrtjtDPwHDO+AO21MJd7yIcr41UetYt5jzaB9Y1UYaPTCnVuJq6S748g1dE11OQlCFIQg+RtAA1SEZIyiBeA==",
"dependencies": {
- "@remix-run/router": "1.7.2",
- "react-router": "6.14.2"
+ "@remix-run/router": "1.14.1",
+ "react-router": "6.21.1"
},
"engines": {
- "node": ">=14"
+ "node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
diff --git a/package.json b/package.json
index 8904574..1c8cf0e 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"react-hot-toast": "^2.4.1",
"react-icons": "^4.8.0",
"react-markdown": "^9.0.0",
- "react-router-dom": "^6.14.2",
+ "react-router-dom": "^6.21.1",
"react-scripts": "5.0.1",
"react-simplemde-editor": "^5.2.0",
"simplemde": "^1.11.2",
diff --git a/src/App.js b/src/App.js
index 0dae341..886134e 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,4 +1,4 @@
-import { HomePage, PostForm, NotFoundPage } from "./pages/index";
+import { HomePage, PostForm, NotFoundPage, SubscriptionPage } from "./pages/index";
import { Routes, Route } from "react-router-dom";
import { PostProvider } from "./context/postContext";
import { Toaster } from "react-hot-toast";
@@ -10,6 +10,7 @@ import Navbar from "./components/Navbar/Navbar";
import ContactForm from "./pages/Contact";
import { DefaultCarousel } from "./components/Carousel/Carousel";
import UserList from "./pages/Users";
+import { PostDetailsCard } from "./components/PostDetailsCard";
function App() {
return (
@@ -23,11 +24,12 @@ function App() {
} />
} />
} />
-
+ } />
+ } />
{/* Private routes */}
}>
} />
- } />
+ } />
} />
} />
diff --git a/src/components/Comments.jsx b/src/components/Comments.jsx
new file mode 100644
index 0000000..b848635
--- /dev/null
+++ b/src/components/Comments.jsx
@@ -0,0 +1,128 @@
+import React from "react";
+import moment from "moment";
+import axios from "axios";
+import toast from "react-hot-toast";
+import Cookies from "js-cookie";
+import { useState } from "react";
+
+export const Comment = ({ comment, onDelete, onEdit }) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editedText, setEditedText] = useState(comment.text);
+
+ const handleDeleteComment = async () => {
+ try {
+ await axios.delete(
+ `http://localhost:4000/api/posts/${comment._id}/comments/`,
+ {
+ withCredentials: true,
+ }
+ );
+
+ onDelete(comment._id);
+
+ toast.success("Comment successfully deleted.");
+ } catch (error) {
+ toast.error("Error deleting the comment", error);
+ }
+ };
+
+ const handleEditComment = async () => {
+ try {
+ const response = await axios.put(
+ `http://localhost:4000/api/posts/${comment._id}/comments/`,
+ { text: editedText },
+ {
+ withCredentials: true,
+ }
+ );
+
+ if (onEdit) {
+ onEdit(response.data);
+ }
+
+ toast.success("Comment successfully edited.");
+ setIsEditing(false);
+ } catch (error) {
+ toast.error("Error editing the comment", error);
+ }
+ };
+
+ const canEditAndDelete =
+ comment?.commentator?.trim() === Cookies.get("username")?.trim();
+
+ return (
+
+
+
+ {comment.commentator}
+
+
+ -{moment(comment.createdAt).format("DD/MM/YYYY")}
+
+ {canEditAndDelete && (
+ <>
+ {isEditing ? (
+ <>
+
+ Save
+
+ setIsEditing(false)}
+ >
+ Cancel
+
+ >
+ ) : (
+ <>
+ setIsEditing(true)}
+ >
+ Edit
+
+
+ Delete
+
+ >
+ )}
+ >
+ )}
+
+
+
+ {isEditing ? (
+
+ );
+};
+
+export const Comments = ({ comments, onDeleteComment, onEditComment }) => (
+
+ {comments.map((comment) => (
+
+ ))}
+
+);
diff --git a/src/components/Navbar/Location.jsx b/src/components/Navbar/Location.jsx
new file mode 100644
index 0000000..44ab7f9
--- /dev/null
+++ b/src/components/Navbar/Location.jsx
@@ -0,0 +1,18 @@
+import { useLocation } from "react-router-dom";
+
+export const useRouteVariables = () => {
+ const location = useLocation();
+
+ return {
+ isLoginPage: location.pathname === "/login",
+ isHomePage: location.pathname === "/",
+ isSignupPage: location.pathname === "/signup",
+ isFormPage: location.pathname === "/new",
+ isAdminPage: location.pathname === "/admin",
+ userListPage: location.pathname === "/users",
+ isContactPage: location.pathname === "/contact",
+ isSubscribePage: location.pathname === "/subscription",
+ isDetailPage: location.pathname.startsWith("/post/"),
+ hideHomeLink: location.pathname.startsWith("/posts/"),
+ };
+};
diff --git a/src/components/Navbar/Navbar.jsx b/src/components/Navbar/Navbar.jsx
index feaf65a..3a2098d 100644
--- a/src/components/Navbar/Navbar.jsx
+++ b/src/components/Navbar/Navbar.jsx
@@ -1,20 +1,25 @@
import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons";
-import { Link, useLocation } from "react-router-dom";
+import { Link } from "react-router-dom";
import useAuth from "../../hooks/useAuth";
import Cookies from "js-cookie";
-
+import { useLocation } from "react-router-dom";
+import { useRouteVariables } from "./Location";
const Navbar = () => {
const location = useLocation();
- const isLoginPage = location.pathname === "/login";
- const isHomePage = location.pathname === "/";
- const isSignupPage = location.pathname === "/signup";
- const isFormPage = location.pathname === "/new";
- const isAdminPage = location.pathname === "/admin";
- const userListPage = location.pathname === "/users";
- const isContactPage = location.pathname === "/contact";
-
+ const {
+ isLoginPage,
+ isSignupPage,
+ hideHomeLink,
+ isAdminPage,
+ isDetailPage,
+ isFormPage,
+ userListPage,
+ isSubscribePage,
+ isHomePage,
+ isContactPage,
+ } = useRouteVariables();
const { setAuth } = useAuth();
const handleLogout = () => {
@@ -32,7 +37,7 @@ const Navbar = () => {
if (isLoginPage || isSignupPage) {
return null;
}
- const hideHomeLink = location.pathname.startsWith("/posts/");
+
return (
@@ -70,7 +75,7 @@ const Navbar = () => {
open ? "flex" : "hidden"
} pb-4 md:pb-0 md:flex md:justify-end md:flex-row navbar-transition animate-flip-down duration-700`}
>
- { !hideHomeLink &&!isAdminPage && !isFormPage && !userListPage && (
+ {!hideHomeLink && !isAdminPage && !isFormPage && !userListPage && (
{
Home
)}
+ {!hideHomeLink &&
+ !isAdminPage &&
+ !isFormPage &&
+ !userListPage &&
+ !isSubscribePage && (
+
+ Subscribe
+
+ )}
{!isHomePage && isAdminPage && (
{
{(!isHomePage || (userListPage && !isContactPage)) &&
!isAdminPage &&
- location.pathname !== "/contact" && (
+ !isSubscribePage &&
+ location.pathname !== "/contact" &&
+ !isDetailPage && (
;
+ return ;
} else if (
extension === "mp4" ||
extension === "webm" ||
extension === "ogv"
) {
return (
-
+
);
} else {
return Unsupported file type
;
@@ -31,7 +39,6 @@ export function insertMedia(filePath) {
export function PostCard({ post }) {
const { deletePost } = usePosts();
const navigate = useNavigate();
-
const normalDate = moment(post.createdAt).format("DD/MM/YYYY");
const handleDelete = (id) => {
@@ -101,7 +108,7 @@ export function PostCard({ post }) {
navigate(`/posts/${post._id}`)}
+ onClick={() => navigate(`/edit/${post._id}`)}
>
@@ -120,17 +127,23 @@ export function PostCard({ post }) {
- {post.image && insertMedia(post.image.url)}
-
-
-
{post.title}
+
+ {post.image && insertMedia(post.image.url)}
+
+
{post.title}
-
- {post.description}
-
+
+ {post.description
+ ? `${post.description.substr(0, 330)}...Read More`
+ : "not description"}
+
- Source: {post.source}
-
+
+ {" "}
+ Source: {post.source}
+
+
+
);
}
diff --git a/src/components/PostCardUser.jsx b/src/components/PostCardUser.jsx
index a696402..c2791cf 100644
--- a/src/components/PostCardUser.jsx
+++ b/src/components/PostCardUser.jsx
@@ -2,55 +2,45 @@ import moment from "moment";
import ReactMarkdown from "react-markdown";
import { insertMedia } from "./PostCard";
import logoImg from "../Images/postimg.jpg";
+import { Link } from "react-router-dom";
export function PostCardUser({ post }) {
const normalDate = moment(post.createdAt).format("DD/MM/YYYY");
return (
-
-
-
-
-
-
- {post.author}
-
-
- {normalDate}
-
-
{post.categories}
+
+
+
+
+
+
+
+ {post.author}
+
+
+ {normalDate}
+
+
{post.categories}
+
-
-
-
+
- {post.image && insertMedia(post.image.url)}
+ {post.image && insertMedia(post.image.url)}
-
-
{post.title}
-
- {post.description}
-
-
- {" "}
- Source: {post.source}
-
-
-
+
+
{post.title}
+
+ {post.description
+ ? `${post.description.substr(0, 330)}...Read More`
+ : "not description"}
+
+
+
+ {" "}
+ Source: {post.source}
+
+
+
+
);
}
diff --git a/src/components/PostDetailsCard.jsx b/src/components/PostDetailsCard.jsx
new file mode 100644
index 0000000..184494e
--- /dev/null
+++ b/src/components/PostDetailsCard.jsx
@@ -0,0 +1,156 @@
+import React, { useState } from "react";
+import moment from "moment";
+import ReactMarkdown from "react-markdown";
+import { insertMedia } from "./PostCard";
+import logoImg from "../Images/postimg.jpg";
+import axios from "axios";
+import { useLocation } from "react-router-dom";
+import { Comments } from "./Comments";
+import toast from "react-hot-toast";
+import { SocialShare } from "./SocialShare";
+import { usePosts } from "../context/postContext";
+
+export function PostDetailsCard() {
+ const { state } = useLocation();
+ const normalDate = moment(state.post.createdAt).format("DD/MM/YYYY");
+ const [commentText, setCommentText] = useState("");
+ const { getPost } = usePosts();
+ const [postData, setPostData] = useState(state.post);
+
+ const handleCommentSubmit = async (e) => {
+ e.preventDefault();
+
+ const postId = state.post._id;
+
+ try {
+ const response = await axios.post(
+ `http://localhost:4000/api/posts/${postId}/comments`,
+ { text: commentText },
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${""}`,
+ },
+ }
+ );
+
+ if (response.status === 201) {
+ toast.success("Successful comment");
+ setCommentText("");
+ const updatedPost = await getPost(postId);
+ setPostData(updatedPost);
+ } else {
+ toast.error("Error posting comment");
+ }
+ } catch (error) {
+ toast.error("This didn't work.");
+ }
+ };
+
+ const handleDeleteComment = async (deletedCommentId) => {
+ try {
+ setPostData((prevPostData) => ({
+ ...prevPostData,
+ comments: prevPostData.comments.filter(
+ (comment) => comment._id !== deletedCommentId
+ ),
+ }));
+ } catch (error) {
+ toast.error("Error deleting comment", error);
+ }
+ };
+
+ const handleEditComment = async (editedComment) => {
+ try {
+ setPostData((prevPostData) => ({
+ ...prevPostData,
+ comments: prevPostData.comments.map((comment) =>
+ comment._id === editedComment._id ? editedComment : comment
+ ),
+ }));
+ } catch (error) {
+ toast.error("Error editing comment", error);
+ }
+ };
+ return (
+
+
+
+
+
+
+ {postData.author}
+
+
+ {normalDate}
+
+
{postData.categories}
+
+
+
+
+
+
+
+ {postData.image && insertMedia(postData.image.url)}
+
+
+
+ {postData.title}
+
+
+ {postData.description}
+
+
+ Source: {postData.source}
+
+
+
+
+
+ {postData.comments.length} Comment(s)
+
+
+
+
+
+ );
+}
diff --git a/src/components/SocialShare.jsx b/src/components/SocialShare.jsx
new file mode 100644
index 0000000..fee3e23
--- /dev/null
+++ b/src/components/SocialShare.jsx
@@ -0,0 +1,39 @@
+import { Dropdown } from "flowbite-react";
+import { FaFacebook, FaWhatsapp, FaLinkedin } from "react-icons/fa";
+
+export function SocialShare() {
+ return (
+
+
+ TechTalk
+
+ help@techtalk12.com
+
+
+
+ Facebook
+
+
+ WhatsApp
+
+
+ Linkedin
+
+
+ );
+}
diff --git a/src/pages/HomePage.js b/src/pages/HomePage.js
index 501e51c..90918a9 100644
--- a/src/pages/HomePage.js
+++ b/src/pages/HomePage.js
@@ -20,11 +20,11 @@ export function HomePage() {
);
}
- const reversedPosts = [...posts].reverse();
+
return (
- {reversedPosts.map((post) => (
+ {posts.map((post) => (
diff --git a/src/pages/HomePageUser.js b/src/pages/HomePageUser.js
index d2ba200..3b3b0b5 100644
--- a/src/pages/HomePageUser.js
+++ b/src/pages/HomePageUser.js
@@ -21,13 +21,16 @@ export function HomePageUser() {
);
}
- const reversedPosts = [...posts].reverse();
+
return (
-
- {reversedPosts.map(post => (
-
- ))}
-
- );
-}
+
+ {posts.map((post) => (
+
+
+
+ ))}
+
+
+ );
+ }
diff --git a/src/pages/PostForm.js b/src/pages/PostForm.js
index f82d82a..0307ecf 100644
--- a/src/pages/PostForm.js
+++ b/src/pages/PostForm.js
@@ -51,7 +51,6 @@ export function PostForm() {
title: Yup.string().required("Title is Required"),
description: Yup.string().required("Description is Required"),
categories: Yup.string().transform((value, originalValue) => {
- // Convert the value to a string
return originalValue ? originalValue.toString() : value;
}).required("Categories is Required"),
source: Yup.string(),
diff --git a/src/pages/SubscribePage.js b/src/pages/SubscribePage.js
new file mode 100644
index 0000000..e448856
--- /dev/null
+++ b/src/pages/SubscribePage.js
@@ -0,0 +1,125 @@
+import React, { useState } from "react";
+import axios from "axios";
+import { toast } from "react-hot-toast";
+import { Link } from "react-router-dom";
+
+export const SubscriptionPage = () => {
+ const [email, setEmail] = useState("");
+
+ const handleSubscribe = async (e) => {
+ e.preventDefault();
+
+ if (!email.trim()) {
+ toast.error("Please enter your email address.");
+ return;
+ }
+
+ try {
+ const response = await axios.post(
+ "http://localhost:4000/api/users/activate-subscription",
+ {
+ email: email,
+ }
+ );
+
+ if (response.status === 200) {
+ toast.success("Subscription successful!");
+ setEmail("");
+ } else {
+ toast.error("Failed to subscribe");
+ }
+ } catch (error) {
+ if (error.response && error.response.status === 404) {
+ toast.error("Email not registered with us.");
+ } else {
+ toast.error("An error occurred:", error);
+ }
+ }
+ };
+
+ const handleUnsubscribe = async (e) => {
+ e.preventDefault();
+
+ if (!email.trim()) {
+ toast.error("Please enter your email address.");
+ return;
+ }
+
+ try {
+ const response = await axios.post(
+ "http://localhost:4000/api/users/unsubscribe",
+ {
+ email: email,
+ }
+ );
+
+ if (response.status === 200) {
+ toast.success("Unsubscribe successful!");
+ setEmail("");
+ } else {
+ toast.error("Failed to unsubscribe");
+ }
+ } catch (error) {
+ if (error.response && error.response.status === 404) {
+ toast.error("Email not registered with us.");
+ } else {
+ toast.error("An error occurred:", error);
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ Do you want to receive notifications when there is a new post?
+
+
+ Enter your email address to which you wish to receive notifications
+ from us. If you are already subscribed and want to unsubscribe,
+ enter your email and click on the unsubscribe button.
+
+
+
+
setEmail(e.target.value)}
+ className="bg-transparent border-2 rounded-full py-4 px-6 text-[16px] leading-[22.4px] font-light placeholder:text-white text-white"
+ placeholder="E-mail Address"
+ />
+
+
+ Unsubscribe
+
+
+ Subscribe
+
+
+
+
+
+ );
+};
diff --git a/src/pages/index.js b/src/pages/index.js
index efc7606..ed83361 100644
--- a/src/pages/index.js
+++ b/src/pages/index.js
@@ -1,3 +1,4 @@
export {HomePage }from "./HomePage";
export {PostForm} from "./PostForm";
export {NotFoundPage} from "./NotFoundPage";
+export {SubscriptionPage} from './SubscribePage';
\ No newline at end of file