diff --git a/client/src/Services/communityService.js b/client/src/Services/communityService.js index 698d94b..54cf939 100644 --- a/client/src/Services/communityService.js +++ b/client/src/Services/communityService.js @@ -1,5 +1,6 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import thaliaAPI from "../API/thaliaAPI"; +import { toast } from "react-toastify"; export const getRecentDiscussions = async (page) => { try { @@ -73,7 +74,6 @@ export const getMyCommunity = createAsyncThunk( return thunkAPI.rejectWithValue(payload) } }) - //New Services export const getCommunitys = async () => { try { @@ -98,4 +98,52 @@ export const UnblockCommunitys = async (communityId) => { } catch (error) { console.log(error) } -} \ No newline at end of file +} + +export const getAllCommunity = createAsyncThunk( + "community/getAllCommunity", + async (_, thunkAPI) => { + try { + const { data } = await thaliaAPI.get('/community/get-suggestions'); + return data; + } catch (err) { + const payload = { + status: err.response.data.status, + message: err.response.data.message + } + return thunkAPI.rejectWithValue(payload) + } + }) + +export const joinCommunity = createAsyncThunk( + "community/joinCommunity", + async (community_id, thunkAPI) => { + try { + const { data } = await thaliaAPI.post('/community/join', { community_id }); + return data; + } catch (err) { + const payload = { + status: err.response.data.status, + message: err.response.data.message + } + return thunkAPI.rejectWithValue(payload) + } + }) + +export const getDiscussions = async (id, pagination) => { + try { + const response = await thaliaAPI.get(`/community/discussions/${id}?page=${pagination}`); + return response.data; + } catch (error) { + toast.error("error while fetching discussion") + } +} + +export const createDiscussion = async (payload) => { + try { + const response = await thaliaAPI.post('/community/discussions', payload); + return response.data; + } catch (error) { + toast.error(error.response?.data.message) + } +} \ No newline at end of file diff --git a/client/src/components/community/CommunityCard/CommunityCard.jsx b/client/src/components/community/CommunityCard/CommunityCard.jsx index 5910a14..f7a79ee 100644 --- a/client/src/components/community/CommunityCard/CommunityCard.jsx +++ b/client/src/components/community/CommunityCard/CommunityCard.jsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; +import { joinCommunity } from "../../../Services/communityService"; export default function CommunityCard({ community, type }) { const navigate = useNavigate(); @@ -32,7 +33,7 @@ export default function CommunityCard({ community, type }) { className="w-full h-28 object-cover" /> ) : ( -
+

{typeof community?.community_name === "string" && @@ -65,8 +66,10 @@ export default function CommunityCard({ community, type }) { ) : ( diff --git a/client/src/components/community/DiscoverCommunity/DiscoverCommunity.jsx b/client/src/components/community/DiscoverCommunity/DiscoverCommunity.jsx new file mode 100644 index 0000000..77d4668 --- /dev/null +++ b/client/src/components/community/DiscoverCommunity/DiscoverCommunity.jsx @@ -0,0 +1,40 @@ +import { useEffect } from "react"; +// import { getAllCommunity } from "../../services/communityService"; +import { useDispatch, useSelector } from "react-redux"; +import { resetCommunity } from "../../../features/communitySlice"; +import { getAllCommunity } from "../../../Services/communityService"; +import CommunityCard from "../CommunityCard/CommunityCard"; + +export default function DiscoverCommunity() { + const dispatch = useDispatch(); + const { community, isSuccess, isError } = useSelector( + (state) => state.community + ); + const { user } = useSelector((state) => state.auth); + useEffect(() => { + dispatch(getAllCommunity()); + }, [dispatch]); + useEffect(() => { + dispatch(resetCommunity()); + }, [isSuccess, isError, dispatch]); + + return ( +
+

Discover Community

+ {community.map((item, index) => { + const members = item.members + .filter((member) => member.status === "active") + .map((com) => com.user_id); + if (!members.includes(user?._id)) { + return ( + + ); + } + })} +
+ ); +} diff --git a/client/src/components/community/DiscussionCard/DiscussionCard.jsx b/client/src/components/community/DiscussionCard/DiscussionCard.jsx index b5f06e3..844a3ad 100644 --- a/client/src/components/community/DiscussionCard/DiscussionCard.jsx +++ b/client/src/components/community/DiscussionCard/DiscussionCard.jsx @@ -108,174 +108,161 @@ export default function DiscussionCard({ discussion, type, setDiscussion }) { }, [discussion]); return ( -
-
-
- {type === "RECENT" ? ( -
- {community?.icon ? ( - - ) : ( - community?.community_name && ( -
- - {community.community_name[0].toUpperCase()} - -
- ) - )} -
-

- {community?.community_name} -

- - Posted by @ - {discussion.userProfile.username} - -
-
- ) : ( -
- {discussion.userProfile.profile_img ? ( - - ) : ( -
- - {discussion.userProfile.email[0].toUpperCase()} +
+
+ {type === "RECENT" ? ( +
+ {community?.icon ? ( + + ) : ( + community?.community_name && ( +
+ + {community.community_name[0].toUpperCase()}
- )} -
-

- {discussion.userProfile.username} -

-
+ ) + )} +
+

+ {community?.community_name} +

+ + Posted by @ + {discussion.userProfile.username} +
- )} -
setOpenList(!openList)} - > -
- {openList && ( -
- - {discussion.user_id === user?._id ? ( - - Delete - - ) : ( - - Report - - )} - -
- )} -
-
- {discussion.content_type === "TEXT" ? ( -

{discussion.content}

- ) : ( - <> -

- {discussion.caption} -

- - - )} -
-
-
- {discussion.likes.includes(user?._id) ? ( -
- handleDislike(discussion._id) + ) : ( +
+ {discussion.userProfile.profile_img ? ( + - -
+ alt="" + className="w-10 h-10 rounded-md" + /> ) : ( -
- handleLike(discussion._id) - } - > - +
+ + {discussion.userProfile.email[0].toUpperCase()} +
)} - {discussion.likes.length} likes +
+

+ {discussion.userProfile.username} +

+
-
- + )} +
setOpenList(!openList)} + > + +
+ {openList && ( +
+ + {discussion.user_id === user?._id ? ( + + Delete + + ) : ( + Report + )} + +
+ )} +
+
+ {discussion.content_type === "TEXT" ? ( +

{discussion.content}

+ ) : ( + <> +

+ {discussion.caption} +

+ + + )} +
+
+
+ {discussion.likes.includes(user?._id) ? (
{ - if (isCommentOpen === "hidden") { - setIsCommentOpen("block"); - } else { - setIsCommentOpen("hidden"); - } - }} + className="dislike-button text-accent text-2xl" + onClick={() => handleDislike(discussion._id)} > - {discussion.comments} comments +
-
-
-
-

comments

-
- - setNewComment(e.target.value) - } - /> + ) : (
handleLike(discussion._id)} > - +
+ )} + {discussion.likes.length} likes +
+
+ +
{ + if (isCommentOpen === "hidden") { + setIsCommentOpen("block"); + } else { + setIsCommentOpen("hidden"); + } + }} + > + {discussion.comments} comments
- {comments.map((item, index) => { - return ( - - ); - })}
-
+

+
+

comments

+
+ setNewComment(e.target.value)} + /> +
+ +
+
+ {comments.map((item, index) => { + return ( + + ); + })} +
); } diff --git a/client/src/components/community/NewDiscussion/NewDiscussion.jsx b/client/src/components/community/NewDiscussion/NewDiscussion.jsx new file mode 100644 index 0000000..30d18d5 --- /dev/null +++ b/client/src/components/community/NewDiscussion/NewDiscussion.jsx @@ -0,0 +1,86 @@ +import { Modal } from "flowbite-react"; +import { useState } from "react"; +import { toast } from "react-toastify"; +import PropTypes from "prop-types"; +import { useSelector } from "react-redux"; +import { createDiscussion } from "../../../Services/communityService"; +import aiThaliaAPI from "../../../API/aiThaliaAPI"; + +export default function NewDiscussion({ + openModal, + setOpenModal, + community, + setDiscussion, +}) { + const { user } = useSelector((state) => state.auth); + const [content, setContent] = useState(""); + async function onSubmit() { + if (content && user && community) { + const payload = { + user_id: user?._id, + community_id: community?._id, + content, + content_type: "TEXT", + }; + const isToxic = await aiThaliaAPI.post("/toxic", { + text: content, + }); + if (isToxic.label === "toxic" && isToxic.score > 0.8) { + console.log("hai"); + } + const response = await createDiscussion(payload); + if (response.discussion) { + toast.success("new discussion added"); + setDiscussion((current) => [ + response.discussion, + ...current, + ]); + setContent(""); + setOpenModal(false); + } + } + } + return ( + setOpenModal(false)} + popup + > + + +

New Discussion

+ +
+ + +
+
+
+ ); +} + +NewDiscussion.propTypes = { + openModal: PropTypes.bool.isRequired, + setOpenModal: PropTypes.func.isRequired, + community: PropTypes.object.isRequired, + setDiscussion: PropTypes.func.isRequired, +}; diff --git a/client/src/components/community/YourCommunity/YourCommunity.jsx b/client/src/components/community/YourCommunity/YourCommunity.jsx index 2e80095..20eb366 100644 --- a/client/src/components/community/YourCommunity/YourCommunity.jsx +++ b/client/src/components/community/YourCommunity/YourCommunity.jsx @@ -12,7 +12,7 @@ export default function YourCommunity() { return (

your communities

-
+
{myCommunity.map((item, index) => { return ( { state.isLoading = false; @@ -57,12 +52,7 @@ const authSlice = createSlice({ state.isSuccess = true; state.user = action.payload.user; localStorage.setItem("user", JSON.stringify(action.payload.user)); - Cookies.set("token", action.payload.token, { - secure: true, - sameSite: "none", - path: '/', - expires: 3, - }); + Cookies.set("token", action.payload.token, { expires: 3, sameSite: 'none', secure: true }); }) .addCase(signUp.rejected, (state, action) => { state.isLoading = false; diff --git a/client/src/features/communitySlice.js b/client/src/features/communitySlice.js index d672c75..c14384a 100644 --- a/client/src/features/communitySlice.js +++ b/client/src/features/communitySlice.js @@ -1,5 +1,5 @@ import { createSlice } from "@reduxjs/toolkit"; -import { getMyCommunity, newCommunity } from "../Services/communityService"; +import { getAllCommunity, getMyCommunity, joinCommunity, newCommunity } from "../Services/communityService"; import { toast } from "react-toastify"; @@ -43,7 +43,7 @@ export const communitySlice = createSlice({ }) - + //get my community builder.addCase(getMyCommunity.pending, (state) => { state.isLoading = true; @@ -60,53 +60,43 @@ export const communitySlice = createSlice({ }) - // //get all community - // builder.addCase(getAllCommunity.pending, (state) => { - // state.isLoading = true; - // }) - // builder.addCase(getAllCommunity.fulfilled, (state, action) => { - // state.isLoading = false; - // state.community = action.payload?.community - // state.isSuccess = true; - // }) - // builder.addCase(getAllCommunity.rejected, (state, action) => { - // state.isError = true; - // state.isLoading = false; - // const error = action.payload as { - // message: string, - // status: number - // }; - // state.errorMessage = error.message; - // state.status = error.status; + //get all community + builder.addCase(getAllCommunity.pending, (state) => { + state.isLoading = true; + }) + builder.addCase(getAllCommunity.fulfilled, (state, action) => { + state.isLoading = false; + state.community = action.payload?.community + state.isSuccess = true; + }) + builder.addCase(getAllCommunity.rejected, (state) => { + state.isError = true; + state.isLoading = false; + toast.error('error while fetching community') + }) - // }) + //join community + builder.addCase(joinCommunity.pending, (state) => { + state.isLoading = true; + }) + builder.addCase(joinCommunity.fulfilled, (state, action) => { + state.isLoading = false; + state.community = state.community.map((item) => { + if (item._id === action.payload.member.community_id) { + item.members.push(action.payload.member); + } + return item; + }) + state.myCommunity = [{ ...action.payload?.community, members: action.payload?.members }, ...state.myCommunity] + state.isSuccess = true; + }) + builder.addCase(joinCommunity.rejected, (state) => { + state.isError = true; + state.isLoading = false; + toast.error('error while joining community') + }) - // //join community - // builder.addCase(joinCommunity.pending, (state) => { - // state.isLoading = true; - // }) - // builder.addCase(joinCommunity.fulfilled, (state, action) => { - // state.isLoading = false; - // state.community = state.community.map((item) => { - // if (item._id === action.payload.newMember.community_id) { - // item.members.push(action.payload.newMember); - // } - // return item; - // }) - // state.myCommunity = [{ ...action.payload?.community, members: action.payload?.members }, ...state.myCommunity] - // state.isSuccess = true; - // }) - // builder.addCase(joinCommunity.rejected, (state, action) => { - // state.isError = true; - // state.isLoading = false; - // const error = action.payload as { - // message: string, - // status: number - // }; - // state.errorMessage = error.message; - // state.status = error.status; - // }) // //accept join request // builder.addCase(acceptJoinRequest.pending, (state) => { // state.isLoading = true; diff --git a/client/src/pages/UserPages/Community/Community.jsx b/client/src/pages/UserPages/Community/Community.jsx index f43b770..72d2097 100644 --- a/client/src/pages/UserPages/Community/Community.jsx +++ b/client/src/pages/UserPages/Community/Community.jsx @@ -10,6 +10,7 @@ import { useNavigate, useParams } from "react-router-dom"; import NewCommunity from "../../../components/community/NewCommunity"; import RecentDiscussions from "../../../components/community/RecentDiscussions/RecentDiscussions"; import YourCommunity from "../../../components/community/YourCommunity/YourCommunity"; +import DiscoverCommunity from "../../../components/community/DiscoverCommunity/DiscoverCommunity"; export default function Community() { const { tab } = useParams(); @@ -114,6 +115,8 @@ export default function Community() { ) : currentTab === "YOUR_COMMUNITY" ? ( + ) : currentTab === "DISCOVER" ? ( + ) : null}
diff --git a/client/src/pages/UserPages/Community/ViewCommunity.css b/client/src/pages/UserPages/Community/ViewCommunity.css new file mode 100644 index 0000000..0d8ef2a --- /dev/null +++ b/client/src/pages/UserPages/Community/ViewCommunity.css @@ -0,0 +1,6 @@ +.view-community .left-side-bar { + height: calc(100vh - 55px); +} +.view-community .right-section { + height: calc(100vh - 70px); +} \ No newline at end of file diff --git a/client/src/pages/UserPages/Community/ViewCommunity.jsx b/client/src/pages/UserPages/Community/ViewCommunity.jsx new file mode 100644 index 0000000..bea244d --- /dev/null +++ b/client/src/pages/UserPages/Community/ViewCommunity.jsx @@ -0,0 +1,294 @@ +import { useEffect, useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { RiGitRepositoryPrivateFill } from "react-icons/ri"; +import { FaUserPlus } from "react-icons/fa"; +import { FaFileCirclePlus } from "react-icons/fa6"; +import "./ViewCommunity.css"; +import { useDispatch, useSelector } from "react-redux"; +import { + getCommunity, + getDiscussions, + getMyCommunity, +} from "../../../Services/communityService"; +import DiscussionCard from "../../../components/community/DiscussionCard/DiscussionCard"; +import { resetCommunity } from "../../../features/communitySlice"; +import NewDiscussion from "../../../components/community/NewDiscussion/NewDiscussion"; + +export default function ViewCommunity() { + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [currentCommunity, setCurrentCommunity] = useState(null); + const { user } = useSelector((state) => state.auth); + const { id } = useParams(); + const [discussion, setDiscussion] = useState([]); + const [showDropDown, setShowDropDown] = useState("hidden"); + const pagination = useRef(1); + const { myCommunity, isSuccess, isError } = useSelector( + (state) => state.community + ); + const [newDiscussion, setNewDiscussion] = useState(false); + const [member, setMember] = useState(null); + + async function fetchDiscussion() { + if (currentCommunity) { + const response = await getDiscussions( + currentCommunity._id, + pagination.current + ); + if (response.discussions.length > 0) { + if (pagination.current === 1) { + setDiscussion(response.discussions); + } else { + setDiscussion([ + ...discussion, + ...response.discussions, + ]); + } + } + } + } + + useEffect(() => { + if (currentCommunity) { + fetchDiscussion(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentCommunity]); + + useEffect(() => { + if (currentCommunity && user) { + const member = currentCommunity.members.filter( + (member) => member.user_id === user?._id + ); + setMember(member[0]); + } + }, [currentCommunity, user]); + + useEffect(() => { + if (id) { + (async () => { + const response = await getCommunity(id); + setCurrentCommunity(response.data.community); + })(); + } + }, [id]); + + useEffect(() => { + dispatch(getMyCommunity()); + }, [dispatch]); + useEffect(() => { + dispatch(resetCommunity()); + }, [isSuccess, isError, dispatch]); + + //scroll handler + useEffect(() => { + const handleScroll = () => { + const bodyHeight = document.body.clientHeight; + const scrollHeight = window.scrollY; + const innerHeight = window.innerHeight; + const isAtBottom = bodyHeight - (scrollHeight + innerHeight) < 1; + if (isAtBottom) { + if (discussion.length >= pagination.current * 10) { + pagination.current = pagination.current + 1; + fetchDiscussion(); + } + } + }; + window.addEventListener("scroll", handleScroll); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, discussion]); + + return ( +
+
setShowDropDown("hidden")} + > +
+

Community

+
navigate("/community/discover")} + > +

+ Discover Communities +

+
+ +

+ Your Communities +

+ {myCommunity && + myCommunity.map((item, index) => { + return ( +
{ + navigate( + `/community/view/${item._id}` + ); + }} + > +
+ {item?.icon ? ( + + ) : ( +

+ {item?.community_name[0].toUpperCase()} +

+ )} +
+
+

+ {item.community_name} +

+
+
+ ); + })} +
+
+
+
+
+
+ {currentCommunity?.icon ? ( + + ) : ( +

+ {currentCommunity?.community_name[0].toUpperCase()} +

+ )} + +
+

setOpenDrawer(true)} + > + {currentCommunity?.community_name} +

+
+ {member && member.is_admin && ( + + )} +
+ +
+
+
setOpenMediaUpload(true)} + > + +
+ + +
send
+
+
+ {discussion?.map((item, index) => { + return ( + + ); + })} +
+
+
+

About

+

+ {currentCommunity?.about} +

+ + {currentCommunity?.privacy === "public" ? ( +
+

Public

+

+ Everyone can see discussions +

+
+ ) : ( +
+
+ +

+ Private +

+
+

+ Only the members of this community + are allowed to see discussions +

+
+ )} +

Activity

+
+
+ +
+ + {/* + + {currentCommunity && ( + + )} */} +
+ ); +} diff --git a/client/src/routes/UserRoute.jsx b/client/src/routes/UserRoute.jsx index 77857e0..6f6b598 100644 --- a/client/src/routes/UserRoute.jsx +++ b/client/src/routes/UserRoute.jsx @@ -4,6 +4,7 @@ import Loader from "../components/Loader/Loader1/Loader"; import Authenticate from "../components/Auth/Authenticate"; import Protect from "../components/Auth/Protect"; import Community from "../pages/UserPages/Community/Community"; +import ViewCommunity from "../pages/UserPages/Community/ViewCommunity"; const Home = lazy(() => import("../pages/UserPages/Home/Home")); const Signup = lazy(() => import("../pages/UserPages/Signup/Signup")); @@ -46,6 +47,10 @@ export default function UserRoute() { path={"/community/:tab"} element={} /> + } + /> diff --git a/server/app.js b/server/app.js index e0f04a7..ee10246 100644 --- a/server/app.js +++ b/server/app.js @@ -9,8 +9,7 @@ const { notFound, errorHandler } = require('./middlewares/errorMiddlewares') const userRoute = require('./routes/indexRoute.js') const adminRoute = require('./routes/adminRoute.js') -// const ORIGIN = process.env.NODE_ENV === 'development' ? "http://localhost:4000" : 'https://thalia.vercel.app' -const ORIGIN = ["http://localhost:4000", 'https://thalia.vercel.app'] +const ORIGIN = process.env.NODE_ENV === 'development' ? "http://localhost:4000" : 'https://thalia.vercel.app' const corsConfig = { origin: ORIGIN, credentials: true, @@ -25,6 +24,13 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })) app.use(cookieParser()); app.use(cors(corsConfig)) +app.use((req, res, next) => { + res.setHeader('Access-Control-Allow-Origin', ORIGIN); + res.setHeader('Access-Control-Allow-Credentials', true); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); + res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type'); + next(); +}); //routes app.use('/api', userRoute) diff --git a/server/controller/discussion.js b/server/controller/discussion.js index 28be3a2..ce1aed5 100644 --- a/server/controller/discussion.js +++ b/server/controller/discussion.js @@ -187,6 +187,7 @@ const getDiscussions = async (req, res, next) => { throw new Error('Internal server error') } } catch (error) { + console.log(error) next(error.message) }