diff --git a/Readme.md b/Readme.md index 25e31267109d7dc6dcecc92dd9d7387ca0599706..f94d398efe313159a2ecd2d2c7b882ce79355267 100644 --- a/Readme.md +++ b/Readme.md @@ -1,11 +1,15 @@ +If nodemon not installed already then install it using the command : + + - npm install -g nodemon + To Run both backend and frontend concurrently run this command in the root folder (Startup-app): -npm install concurrently --save-dev + - npm install concurrently --save-dev If required run this command inside the frontend folder (awt_cw/Startup-app/frontend): -npm install + - npm install Finally inside the Startup-app folder run the following command to start backend and frontend concurrently: -npm start + - npm start diff --git a/Startup-app/backend/controllers/ideaController.js b/Startup-app/backend/controllers/ideaController.js index c78ff8a033d3d5c4af5e5aa8cf7e850436f4511e..f4c5deb475c6dfdd93c48a6b460156c76f0e9dd6 100644 --- a/Startup-app/backend/controllers/ideaController.js +++ b/Startup-app/backend/controllers/ideaController.js @@ -101,4 +101,58 @@ exports.getLikedPosts = async (req, res) => { console.error('Error fetching liked posts:', err.message); res.status(500).json({ msg: 'Failed to fetch liked posts' }); } +}; + +// Add or remove a collaborator +exports.toggleCollaboration = async (req, res) => { + try { + //console.log('Request body:', req.body); + //console.log('Authenticated user:', req.user); + + const { ideaId } = req.body; + const userId = req.user.id; + + const idea = await Idea.findById(ideaId); + if (!idea) { + console.error('Idea not found:', ideaId); + return res.status(404).json({ msg: 'Idea not found' }); + } + + const isCollaborating = idea.collaborators.includes(userId); + + if (isCollaborating) { + // Remove collaborator + idea.collaborators = idea.collaborators.filter(id => id.toString() !== userId); + } else { + // Add collaborator + idea.collaborators.push(userId); + } + + await idea.save(); + + res.status(200).json({ + msg: isCollaborating ? 'Collaboration reverted' : 'Collaboration requested', + collaborators: idea.collaborators, + }); + } catch (error) { + console.error('Error toggling collaboration:', error.message); + res.status(500).json({ msg: 'Server error' }); + } +}; + +// Get collaborators for an idea +exports.getCollaborators = async (req, res) => { + try { + const { ideaId } = req.params; + + const idea = await Idea.findById(ideaId).populate('collaborators', 'name email'); + if (!idea) { + return res.status(404).json({ msg: 'Idea not found' }); + } + + res.status(200).json(idea.collaborators); + } catch (error) { + console.error('Error fetching collaborators:', error.message); + res.status(500).json({ msg: 'Server error' }); + } }; \ No newline at end of file diff --git a/Startup-app/backend/models/idea.js b/Startup-app/backend/models/idea.js index 48919d97213c7b6c8843c629077d426e0f80cca6..83da5ac2b7c11c2542261a1baf28d966f08bca29 100644 --- a/Startup-app/backend/models/idea.js +++ b/Startup-app/backend/models/idea.js @@ -25,6 +25,13 @@ const IdeaSchema = new mongoose.Schema({ ref: 'User', default: [], }], + // List of collaborators + collaborators: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + default: [], + }], + }, { timestamps: true }); const Idea = mongoose.models.Idea || mongoose.model('Idea', IdeaSchema); diff --git a/Startup-app/backend/routes/ideaRoutes.js b/Startup-app/backend/routes/ideaRoutes.js index f418141b315000bcc25e42e76f774a5f5ea53bc9..a8e44b3e822830d0a69b05e33fef78533719e262 100644 --- a/Startup-app/backend/routes/ideaRoutes.js +++ b/Startup-app/backend/routes/ideaRoutes.js @@ -1,4 +1,5 @@ const express = require('express'); +const { toggleCollaboration, getCollaborators } = require('../controllers/ideaController'); const router = express.Router(); const auth = require('../middleware/auth'); const multer = require('multer'); @@ -83,13 +84,14 @@ router.get('/feed', auth, async (req, res) => { const currentUserId = req.user.id; const currentUser = await User.findById(currentUserId).select('following'); - const ideas = await Idea.find().sort({ createdAt: -1 }); + const ideas = await Idea.find().populate('collaborators', '_id').sort({ createdAt: -1 }); const followedIds = currentUser?.following?.map(id => id.toString()) || []; const ideasWithFollowStatus = ideas.map(idea => ({ ...idea.toObject(), - followed: followedIds.includes(idea.userId.toString()) + followed: followedIds.includes(idea.userId.toString()), + collaborators: idea.collaborators.map(c => c._id.toString()) })); res.status(200).json(ideasWithFollowStatus); @@ -120,4 +122,21 @@ router.get('/idea-of-the-day', async (req, res) => { } }); +router.post('/collaborate', auth, toggleCollaboration); +router.get('/:ideaId/collaborators', auth, getCollaborators); + +router.put('/:ideaId/revert-collab', async (req, res) => { + const { userId } = req.body; + const idea = await Idea.findById(req.params.ideaId); + if (!idea) return res.status(404).send('Idea not found'); + + idea.collaborators = idea.collaborators.filter( + id => id.toString() !== userId + ); + + await idea.save(); + res.status(200).json({ message: 'Collaboration reverted' }); +}); + module.exports = router; + diff --git a/Startup-app/backend/server.js b/Startup-app/backend/server.js index fadd24f8f629234365653a75af7e40cc34a93e62..47563b0cf50f3c6074ab9c8ade421f0469158dd1 100644 --- a/Startup-app/backend/server.js +++ b/Startup-app/backend/server.js @@ -86,6 +86,9 @@ app.use('/api', followRoutes); app.use('/api', likeRoutes); app.use('/api', followposts); app.use('/api/chat', chatRoutes); +app.use('/api/ideas/collaborate', ideaRoutes); // Collaboration route +app.use('/api/ideas/:ideaId/collaborators', ideaRoutes); // Collaborators route +app.use('/api/ideas/:ideaId/revert-collab', ideaRoutes); // Revert collaboration route // Health check endpoint app.get('/api/health', (req, res) => { diff --git a/Startup-app/frontend/src/components/IdeaOfTheDay.jsx b/Startup-app/frontend/src/components/IdeaOfTheDay.jsx index 49f3e70388f1f12602eff570e1a5c2db445464bc..23b8abdc3675fa8fbc5df110775977a95e7430d9 100644 --- a/Startup-app/frontend/src/components/IdeaOfTheDay.jsx +++ b/Startup-app/frontend/src/components/IdeaOfTheDay.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import confetti from "canvas-confetti"; // ✅ NEW +import confetti from "canvas-confetti"; import axios from "axios"; import { useNavigate } from "react-router-dom"; import "../styles/IdeaOfTheDay.css"; @@ -24,29 +24,30 @@ const IdeaOfTheDay = () => { fetchIdea(); }, []); - // 🎊 Fire confetti when the idea is loaded + // 🎉 Confetti only once per login session useEffect(() => { - if (idea) { + const alreadyShown = localStorage.getItem("confettiShown"); + + if (idea && !alreadyShown) { let duration = 3 * 1000; - let animationEnd = Date.now() + duration; + let end = Date.now() + duration; const interval = setInterval(() => { - const timeLeft = animationEnd - Date.now(); - - if (timeLeft <= 0) { - return clearInterval(interval); - } + if (Date.now() > end) return clearInterval(interval); confetti({ particleCount: 200, spread: 120, origin: { x: Math.random(), - y: Math.random() - 0.2 // allows some to come from top + y: Math.random() - 0.2, }, - colors: ["#bb0000", "#ffffff", "#00cc99", "#ffcc00", "#6633ff"], + colors: ["#bb0000", "#00cc99", "#ffcc00", "#6633ff"], }); }, 300); + + // ✅ Mark as shown + localStorage.setItem("confettiShown", "true"); } }, [idea]); @@ -84,15 +85,10 @@ const IdeaOfTheDay = () => { </span> <span className="text-gray-400"> | </span> - <span><strong>Domain:</strong> {idea.domain}</span> - <span className="text-gray-400"> | </span> - <span><strong>Budget:</strong> ₹{idea.budget}</span> - <span className="text-gray-400"> | </span> - <span><strong>Stage:</strong> {idea.projectStage}</span> </div> diff --git a/Startup-app/frontend/src/pages/Feed.js b/Startup-app/frontend/src/pages/Feed.js index c5492848136153026908da70c4035381a5d28e93..cebb9a992d0fee7d52b665daf3bf1fcd70487ec1 100644 --- a/Startup-app/frontend/src/pages/Feed.js +++ b/Startup-app/frontend/src/pages/Feed.js @@ -52,6 +52,62 @@ const Feed = () => { } }; + const handleCollaboration = async (ideaId) => { + if (!currentUserId) return; + try { + const currentIdea = ideas.find((idea) => idea._id === ideaId); + const wasCollaborating = currentIdea.collaborators.includes(currentUserId); + const response = await axios.post( + 'http://localhost:5001/api/ideas/collaborate', + { ideaId }, + { headers: { Authorization: `Bearer ${token}` } } + ); + setIdeas((prev) => + prev.map((idea) => + idea._id === ideaId + ? { ...idea, collaborators: response.data.collaborators } + : idea + ) + ); + const isCollaborating = response.data.collaborators.includes(currentUserId); + setMessage( + wasCollaborating + ? 'Collaboration reverted successfully!' + : 'Collaboration request sent successfully!' + ); + } catch (error) { + console.error('Collaboration error:', error.response?.data || error.message); + setMessage('Failed to update collaboration status.'); + } + }; + + const handleRevertCollab = async (ideaId) => { + if (!currentUserId) return; + + try { + await axios.put(`http://localhost:5001/api/ideas/${ideaId}/revert-collab`, { + userId: currentUserId + }); + + // Update state after removing collaborator + setIdeas(prevIdeas => + prevIdeas.map(idea => + idea._id === ideaId + ? { + ...idea, + collaborators: idea.collaborators.filter(id => id !== currentUserId) + } + : idea + ) + ); + + setMessage('Collaboration reverted successfully!'); + } catch (error) { + console.error('Error reverting collaboration:', error); + setMessage('Error reverting collaboration.'); + } + }; + const handleFollow = async (userId) => { if (!userId || userId === currentUserId) return; @@ -127,6 +183,7 @@ const Feed = () => { ...idea, followed: serverFollowing.includes(idea.userId), likedBy: idea.likedBy || [], + collaborators: idea.collaborators || [] })); const filteredIdeas = selectedDomain @@ -191,15 +248,30 @@ const Feed = () => { {idea.userId === currentUserId ? ( <span className="posted-by-you">Posted by You</span> ) : ( - <button - className={`follow-btn ${idea.followed ? 'followed' : ''}`} - onClick={(e) => { - e.stopPropagation(); - idea.followed ? handleUnfollow(idea.userId) : handleFollow(idea.userId); - }} - > - {idea.followed ? 'Unfollow' : 'Follow'} - </button> + <> + <div className="button-group"> + <button + className="collaborate-btn" + onClick={(e) => { + e.stopPropagation(); + idea.collaborators.includes(currentUserId) ? handleRevertCollab(idea._id) : handleCollaboration(idea._id); + } } + > + {idea.collaborators.includes(currentUserId) + ? 'Revert Collab' + : 'Collaborate'} + </button> + + <button + className={`follow-btn ${idea.followed ? 'followed' : ''}`} + onClick={(e) => { + e.stopPropagation(); + idea.followed ? handleUnfollow(idea.userId) : handleFollow(idea.userId); + } } + > + {idea.followed ? 'Unfollow' : 'Follow'} + </button> + </div></> )} </div> diff --git a/Startup-app/frontend/src/pages/MyPosts.js b/Startup-app/frontend/src/pages/MyPosts.js index dfc3695cf2703e711cf1d2df338a0e4e01f160fe..118bceed8239a8b4976583a7801836e623402a9d 100644 --- a/Startup-app/frontend/src/pages/MyPosts.js +++ b/Startup-app/frontend/src/pages/MyPosts.js @@ -10,6 +10,26 @@ const MyPosts = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const navigate = useNavigate(); + const [collaborators, setCollaborators] = useState([]); + + const fetchCollaborators = async (ideaId) => { + try { + const token = localStorage.getItem('token'); + const response = await axios.get( + `http://localhost:5001/api/ideas/${ideaId}/collaborators`, + { headers: { Authorization: `Bearer ${token}` } } + ); + // Update the specific post with its collaborators + setPosts((prevPosts) => + prevPosts.map((post) => + post._id === ideaId ? { ...post, collaborators: response.data } : post + ) + ); + //setCollaborators(response.data); + } catch (error) { + console.error('Error fetching collaborators:', error.response?.data || error.message); + } + }; useEffect(() => { const fetchMyPosts = async () => { @@ -20,7 +40,19 @@ const MyPosts = () => { 'x-auth-token': token } }); - setPosts(response.data); + + // Initialize collaborators as an empty array for each post + const postsWithCollaborators = await Promise.all( + response.data.map(async(post) => { + const collaboratorsRes = await axios.get( + `http://localhost:5001/api/ideas/${post._id}/collaborators`, + { headers: { Authorization: `Bearer ${token}` } } + ); + return { ...post, collaborators: collaboratorsRes.data }; + })); + + setPosts(postsWithCollaborators); + //setPosts(response.data); } catch (error) { setError(error.response?.data?.msg || 'Failed to fetch your posts'); console.error("Error fetching your posts:", error); @@ -104,6 +136,23 @@ const MyPosts = () => { ))} </div> )} + + <button + className="collaborators-btn" + onClick={(e) => { + e.stopPropagation(); + fetchCollaborators(post._id); + }} + > + Collaborators ({post.collaborators?.length || 0}) + </button> + {post.collaborators?.length > 0 ? ( + <ul> + {post.collaborators.map((collaborator) => ( + <li key={collaborator._id}>{collaborator.name || collaborator.email}</li> + ))} + </ul> + ) : null} </div> ))} </div> diff --git a/Startup-app/frontend/src/styles/Feed.css b/Startup-app/frontend/src/styles/Feed.css index 32157f529202b1d6b6198d88d4028e94027378a6..ee81f1dc656bffc1b1c775ea4baaa06c35099b3c 100644 --- a/Startup-app/frontend/src/styles/Feed.css +++ b/Startup-app/frontend/src/styles/Feed.css @@ -402,4 +402,31 @@ border-radius: 20px; font-size: 0.85rem; font-weight: 500; +} + +.collaborate-btn, .collaborators-btn { + background-color: #2c7be5; + color: white; + border: none; + padding: 6px 10px; + height: 30px; + font-size: 0.8rem; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; + white-space: nowrap; + width: 130px; + text-align: center; + margin-right: 10px; +} + +.collaborate-btn:hover, .collaborators-btn:hover { + background-color: #0056b3; +} + +.button-group { + display: flex; + justify-content: space-between; + margin-top: 10px; + gap: 10px; } \ No newline at end of file diff --git a/Startup-app/frontend/src/styles/MyPosts.css b/Startup-app/frontend/src/styles/MyPosts.css index 868fea198652299062a88ae76fe9264c07df9335..0e04906010cbe11122243d01b081a5d7e421e882 100644 --- a/Startup-app/frontend/src/styles/MyPosts.css +++ b/Startup-app/frontend/src/styles/MyPosts.css @@ -58,4 +58,24 @@ .create-first-button:hover { background-color: #45a049; + } + + .collaborate-btn, .collaborators-btn { + background-color: #2c7be5; + color: white; + border: none; + padding: 6px 10px; + height: 30px; + font-size: 0.8rem; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; + white-space: nowrap; + width: 150px; + margin-right: 10px; + text-align: center; + } + + .collaborate-btn:hover, .collaborators-btn:hover { + background-color: #0056b3; } \ No newline at end of file