diff --git a/admin/Dockerfile b/admin/Dockerfile new file mode 100644 index 0000000..a37017c --- /dev/null +++ b/admin/Dockerfile @@ -0,0 +1,7 @@ +FROM node:alpine +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm install +COPY . . +EXPOSE 5173 +CMD ["npm", "run" , "dev"] \ No newline at end of file diff --git a/admin/src/pages/ContactMessages.tsx b/admin/src/pages/ContactMessages.tsx index 9c74662..d9d63a8 100644 --- a/admin/src/pages/ContactMessages.tsx +++ b/admin/src/pages/ContactMessages.tsx @@ -9,6 +9,7 @@ import '../styles/Model.css' import { ColorRing } from 'react-loader-spinner'; import { MdMessage } from "react-icons/md"; import { TbReportAnalytics } from "react-icons/tb"; +import toast from "react-hot-toast"; const ContactMessages = () => { const [contactMessages, setContactMessages] = useState([]); @@ -59,6 +60,21 @@ const ContactMessages = () => { } }; + const handleDeleteMessage = async (id: string) => { + try { + await axios.delete(`/api/v1/admin/deletecontactmessage/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + setContactMessages(contactMessages.filter((msg) => msg.id !== id)); + toast.success("Contact Message deleted successfully !") + } catch (error) { + console.error("Error deleting message:", error); + toast.error("Error in deleting message") + } + }; + const handleOpenModal = (message: IContactMessage) => { setSelectedMessage(message); setOpen(true); @@ -114,7 +130,7 @@ const ContactMessages = () => { {new Date(contactMessage.createdAt).toLocaleDateString()} {contactMessage.subject.slice(0, 10)} {contactMessage.message.slice(0, 10)} - + ))} diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..6c5a22a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,10 @@ +FROM node:alpine +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm install +COPY . . +RUN npm run backend-build + + +EXPOSE 8000 +CMD ["npm", "run" , "dev"] \ No newline at end of file diff --git a/backend/src/routes/admin/controller.ts b/backend/src/routes/admin/controller.ts index 18bddf3..ab37e3e 100644 --- a/backend/src/routes/admin/controller.ts +++ b/backend/src/routes/admin/controller.ts @@ -1090,6 +1090,25 @@ export const downloadFavoritesReportController = async (req: Request, res: Respo } }; +export const deleteContactMessage = async (req: Request, res: Response) => { + const { id } = req.params; + + try { + const deletedMessage = await prisma.contactMessage.delete({ + where: { id }, + }); + + res.status(200).json({ + message: "Contact message deleted successfully!", + deletedMessage, + }); + } catch (error) { + res.status(500).json({ + error: "An error occurred while deleting the contact message!", + }); + } +}; + export const deleteFeedback = async (req: Request, res: Response) => { const { id } = req.params; diff --git a/backend/src/routes/admin/route.ts b/backend/src/routes/admin/route.ts index d49966f..29a203b 100644 --- a/backend/src/routes/admin/route.ts +++ b/backend/src/routes/admin/route.ts @@ -1,5 +1,5 @@ import {Router} from 'express'; -import { getPostReactionsController,getFavoritesController,adminLoginController, adminProfileController, allUserForAdmin, blockUserController, unblockUserController, getAdminPostsController, getAdminTrendingPostsController, getAdminStatsController, getGraphsStatsController, updatePostController, deletePostController, getPostByIdController, getAllContactMessages, deleteCommentController, downloadReportController, getFeedbacks, toggleFeedbackVisibility, downloadCommentsReportController, downloadFavoritesReportController, downloadReactionsReportController, downloadUsersReportController, downloadContactMessagesReportController, downloadPostsReportController, deleteFeedback } from './controller'; +import { getPostReactionsController,getFavoritesController,adminLoginController, adminProfileController, allUserForAdmin, blockUserController, unblockUserController, getAdminPostsController, getAdminTrendingPostsController, getAdminStatsController, getGraphsStatsController, updatePostController, deletePostController, getPostByIdController, getAllContactMessages, deleteCommentController, downloadReportController, getFeedbacks, toggleFeedbackVisibility, downloadCommentsReportController, downloadFavoritesReportController, downloadReactionsReportController, downloadUsersReportController, downloadContactMessagesReportController, downloadPostsReportController, deleteContactMessage, deleteFeedback } from './controller'; import { isAdmin } from '../../middleware/adminAuth'; const adminRouter = Router(); @@ -54,6 +54,8 @@ adminRouter.get("/downloadusersfavoritesreport", downloadFavoritesReportControll adminRouter.get("/downloadusersreactionreport", downloadReactionsReportController); +adminRouter.delete("/deletecontactmessage/:id", isAdmin, deleteContactMessage); + adminRouter.delete('/deletefeedback/:id', isAdmin, deleteFeedback); export default adminRouter; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ececd20 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ + +services: + + admin: + build: + context: ./admin + container_name: styleshare-admin + ports: + - '5173:5173' + + frontend: + build: + context: ./frontend + container_name: styleshare-frontend + ports: + - '3000:3000' + + backend: + build: + context: ./backend + container_name: styleshare-backend + depends_on: + - db + environment: + - DATABASE_URL=${DATABASE_URL} + + ports: + - '8000:8000' + + db: + image: mongo:latest + container_name: styleshare-db + restart: always + ports: + - '27017:27017' + volumes: + - mongodb_data:/data/db + +volumes: + mongodb_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..8093374 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,7 @@ +FROM node:alpine +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm install +COPY . . +EXPOSE 3000 +CMD ["npm", "run" , "dev"] \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index eb26e80..c7d02e9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,6 +31,7 @@ "react-fast-marquee": "^1.6.4", "react-hot-toast": "^2.4.1", "react-icons": "^5.2.1", + "react-loading-skeleton": "^3.4.0", "react-parallax-tilt": "^1.7.231", "react-responsive-modal": "^6.4.2", "react-router-dom": "^6.24.0", @@ -4842,6 +4843,14 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-loading-skeleton": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.4.0.tgz", + "integrity": "sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-parallax-tilt": { "version": "1.7.232", "resolved": "https://registry.npmjs.org/react-parallax-tilt/-/react-parallax-tilt-1.7.232.tgz", diff --git a/frontend/package.json b/frontend/package.json index 254593c..dc06007 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "react-fast-marquee": "^1.6.4", "react-hot-toast": "^2.4.1", "react-icons": "^5.2.1", + "react-loading-skeleton": "^3.4.0", "react-parallax-tilt": "^1.7.231", "react-responsive-modal": "^6.4.2", "react-router-dom": "^6.24.0", diff --git a/frontend/src/components/LeaderboardSkeleton.tsx b/frontend/src/components/LeaderboardSkeleton.tsx new file mode 100644 index 0000000..db666b7 --- /dev/null +++ b/frontend/src/components/LeaderboardSkeleton.tsx @@ -0,0 +1,59 @@ +import Skeleton from 'react-loading-skeleton'; +import 'react-loading-skeleton/dist/skeleton.css'; + +const LeaderboardSkeleton = () => { + const skeletonArray = Array(5).fill(0); // Adjust the number of skeleton rows as needed + + return ( +
+ + + + + + + + + + + + {skeletonArray.map((_, index) => ( + + + + + + + + ))} + +
+ + + + + + + + + +
+
+ +
+
+
+ +
+
+ + + + + +
+
+ ); +}; + +export default LeaderboardSkeleton; diff --git a/frontend/src/components/PostCard.tsx b/frontend/src/components/PostCard.tsx index 2d83025..7aae143 100644 --- a/frontend/src/components/PostCard.tsx +++ b/frontend/src/components/PostCard.tsx @@ -9,7 +9,7 @@ import toast from "react-hot-toast"; import { Link } from "react-router-dom"; import { FaEdit } from "react-icons/fa"; import bgHero from "../assets/bgHero.png"; -import { MdFavorite,MdFavoriteBorder } from "react-icons/md"; +import { MdFavorite, MdFavoriteBorder } from "react-icons/md"; type Props = { post: IPost; @@ -184,74 +184,77 @@ const PostCard = ({ post, onDelete, currentUser }: Props) => { }; return ( - -
-

{post.title}

- {isFavorite ? ( - - ) : ( - - )} -
-

- {post.description.length > 100 - ? `${post.description.slice(0, 100)}...` - : post.description} -

-

By : - - - {' '} @{post.author.username}{' '} - - -

- -
- {post.tags.map((tag, index) => ( - - {tag} - - ))} -
-
- - {currentUser && currentUser.id === post.author.id && ( -
- -
+
+ + {currentUser && currentUser.id === post.author.id && ( +
+ -
- )} + + +
+ )} +
+ {/* {renderReactions()} */} ); diff --git a/frontend/src/components/PostsSkeleton.tsx b/frontend/src/components/PostsSkeleton.tsx new file mode 100644 index 0000000..3763a9a --- /dev/null +++ b/frontend/src/components/PostsSkeleton.tsx @@ -0,0 +1,44 @@ +import Skeleton from 'react-loading-skeleton'; +import 'react-loading-skeleton/dist/skeleton.css'; + +const PostsPageSkeleton = () => { + const skeletonArray = Array(9).fill(0); // Create an array with 9 elements for the skeleton placeholders + + return ( +
+ {skeletonArray.map((_, index) => ( +
+
+ + +
+ + + +
+ {Array(3).fill(0).map((_, idx) => ( + + ))} +
+
+
+ {Array(3).fill(0).map((_, idx) => ( + + ))} +
+
+ + +
+
+
+ ))} +
+ ); +}; + +export default PostsPageSkeleton; diff --git a/frontend/src/pages/EditPost.tsx b/frontend/src/pages/EditPost.tsx index e6f0af9..f0efb0b 100644 --- a/frontend/src/pages/EditPost.tsx +++ b/frontend/src/pages/EditPost.tsx @@ -42,7 +42,7 @@ const EditPost = () => { const handleAddTag = () => { if (tagInput.length > 0 && !post.tags.includes(tagInput)) { - setPost({ ...post, tags: [...post.tags, tagInput] }); + setPost({ ...post, tags: [...post.tags, tagInput.toLowerCase()] }); setTagInput(""); } }; diff --git a/frontend/src/pages/LeaderBoard.tsx b/frontend/src/pages/LeaderBoard.tsx index e72b8d5..0da01fc 100644 --- a/frontend/src/pages/LeaderBoard.tsx +++ b/frontend/src/pages/LeaderBoard.tsx @@ -1,10 +1,10 @@ import { useNavigate } from 'react-router-dom'; -import Loader from '../components/Loader'; import { GiTrophyCup } from "react-icons/gi"; import { useRecoilValue } from 'recoil'; import { userState } from '../store/atoms/auth'; import useLeaderboard from '../hooks/useLeadearboard'; import bgHero from "../assets/bgHero.png"; +import LeaderboardSkeleton from '../components/LeaderboardSkeleton'; const LeaderBoard = () => { const { loading, leaderboard } = useLeaderboard(); @@ -24,7 +24,7 @@ const LeaderBoard = () => {
{loading ? (
- +
) : (
diff --git a/frontend/src/pages/NewPost.tsx b/frontend/src/pages/NewPost.tsx index 7ae333d..d0530d9 100644 --- a/frontend/src/pages/NewPost.tsx +++ b/frontend/src/pages/NewPost.tsx @@ -33,7 +33,7 @@ const NewPost = () => { const handleAddTag = () => { if (tagInput.length > 0 && !tags.includes(tagInput)) { - setTags([...tags, tagInput]); + setTags([...tags, tagInput.toLowerCase()]); setTagInput(""); } }; diff --git a/frontend/src/pages/Posts.tsx b/frontend/src/pages/Posts.tsx index d55de4a..e91a37d 100644 --- a/frontend/src/pages/Posts.tsx +++ b/frontend/src/pages/Posts.tsx @@ -1,11 +1,11 @@ import { useEffect, useState, useRef } from "react"; -import Loader from "../components/Loader"; import PostCard from "../components/PostCard"; import { userState } from "../store/atoms/auth"; import { useRecoilValue } from "recoil"; import usePosts from "../hooks/usePosts"; import bgHero from "../assets/bgHero.png"; import { IoIosArrowDown } from "react-icons/io"; +import PostsPageSkeleton from "../components/PostsSkeleton"; const Posts = () => { const currentUser = useRecoilValue(userState); @@ -23,7 +23,7 @@ const Posts = () => { removeTag: deleteTag, searchQuery, setSearchQuery, - fetchPosts, + fetchPosts, } = usePosts({ initialPage: 1, pageSize: 12, @@ -33,9 +33,12 @@ const Posts = () => { const [tagInput, setTagInput] = useState(""); const [filterTags, setFilterTags] = useState([]); const filterRef = useRef(null); - const filteredPosts = posts; - + const allTags = filteredPosts.map(post => post.tags).flat(); + const uniqueTags = [...new Set(allTags)]; + const tagsToDisplay = uniqueTags.slice(0, 3); + var placeholderTags = tagsToDisplay.length > 0 ? tagsToDisplay.join(", ") : ""; + placeholderTags = placeholderTags + (uniqueTags.length > 3 ? " ..." : ""); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (filterRef.current && !filterRef.current.contains(event.target as Node)) { @@ -44,7 +47,7 @@ const Posts = () => { }; document.title = "Style Share | Our Posts 📃"; - + document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); @@ -80,7 +83,7 @@ const Posts = () => { }; if (loading) { - return ; + return ; } if (error) { @@ -93,8 +96,8 @@ const Posts = () => { return (
{ backgroundPosition: "center", }} > -

Posts

+

Posts

@@ -180,56 +183,55 @@ const Posts = () => {
{filteredPosts.length === 0 ? ( -
No Posts
+
No Posts
) : ( -
- {filteredPosts.map((post) => ( - - ))} +
+
+ {filteredPosts.map((post) => ( + + ))} +
+
+ + {Array.from({ length: totalPages }, (_, i) => ( + + ))} + +
)} -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
);