From e13624f7dbb852f4dcc48f6244513b7830f8d1d4 Mon Sep 17 00:00:00 2001 From: "kruthik.soundapppan@gmail.com" <kruthik.soundapppan@gmail.com> Date: Mon, 21 Apr 2025 17:50:43 +0100 Subject: [PATCH] Added likes tracking feature in MyPosts --- Startup-app/backend/models/idea.js | 19 +- Startup-app/backend/routes/ideaRoutes.js | 61 +++- Startup-app/backend/routes/likeRoutes.js | 31 +- Startup-app/frontend/package-lock.json | 331 ++++++++++++++++++ Startup-app/frontend/package.json | 1 + .../frontend/src/components/IdeaOfTheDay.jsx | 7 +- .../frontend/src/components/LikesChart.jsx | 85 +++++ Startup-app/frontend/src/pages/MyPosts.js | 131 ++++--- .../frontend/src/styles/LikesChart.css | 35 ++ 9 files changed, 618 insertions(+), 83 deletions(-) create mode 100644 Startup-app/frontend/src/components/LikesChart.jsx create mode 100644 Startup-app/frontend/src/styles/LikesChart.css diff --git a/Startup-app/backend/models/idea.js b/Startup-app/backend/models/idea.js index 83da5ac2..42e937ef 100644 --- a/Startup-app/backend/models/idea.js +++ b/Startup-app/backend/models/idea.js @@ -1,7 +1,7 @@ const mongoose = require('mongoose'); const IdeaSchema = new mongoose.Schema({ - email:{type:String, required: true}, + email: { type: String, required: true }, userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, name: { type: String, required: true }, title: { type: String, required: true }, @@ -10,12 +10,10 @@ const IdeaSchema = new mongoose.Schema({ budget: { type: Number, required: true }, projectStage: { type: String, required: true }, location: { type: String, required: true }, - file: { name: String, type: String, data: String, size:Number, required: true }, + file: { name: String, type: String, data: String, size: Number, required: true }, fileId: mongoose.Schema.Types.ObjectId, - - - // 👠New fields for like feature + // 👠Like feature likes: { type: Number, default: 0, @@ -25,13 +23,20 @@ const IdeaSchema = new mongoose.Schema({ ref: 'User', default: [], }], - // List of collaborators + + // ✅ Like timestamps for chart + likeTimestamps: { + type: Map, + of: [Date], + default: {} + }, + + // 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 a8e44b3e..1a722c98 100644 --- a/Startup-app/backend/routes/ideaRoutes.js +++ b/Startup-app/backend/routes/ideaRoutes.js @@ -38,19 +38,16 @@ router.get('/count', async (req, res) => { /** * ✅ Get posts created by a specific user - * URL: /api/ideas/posts-by-user/:userId */ router.get('/posts-by-user/:userId', getUserPosts); /** * ✅ Get posts liked by a user - * URL: /api/ideas/liked-by-user/:userId */ router.get('/liked-by-user/:userId', getLikedPosts); /** * ✅ Get posts created by the currently logged-in user - * URL: /api/ideas/my-posts */ router.get('/my-posts', auth, async (req, res) => { try { @@ -66,18 +63,16 @@ router.get('/my-posts', auth, async (req, res) => { /** * ✅ Submit an idea with file upload - * URL: /api/ideas/submit */ router.post('/submit', auth, upload.single('file'), submitIdea); /** - * ✅ Get all ideas (optional usage) + * ✅ Get all ideas */ router.get('/all', auth, getIdeas); /** - * ✅ Get all ideas for the feed with follow status - * URL: /api/feed + * ✅ Get all ideas for the feed */ router.get('/feed', auth, async (req, res) => { try { @@ -103,7 +98,6 @@ router.get('/feed', auth, async (req, res) => { /** * ✅ Get the idea with the highest likes (Idea of the Day) - * URL: /api/ideas/idea-of-the-day */ router.get('/idea-of-the-day', async (req, res) => { try { @@ -122,6 +116,56 @@ router.get('/idea-of-the-day', async (req, res) => { } }); +/** + * ✅ Get like counts per day for the past 30 days + * URL: /api/ideas/:ideaId/likes-per-day + */ +router.get('/:ideaId/likes-per-day', auth, async (req, res) => { + try { + const { ideaId } = req.params; + const startDate = new Date(); + startDate.setDate(startDate.getDate() - 29); + startDate.setHours(0, 0, 0, 0); + + const result = await Idea.aggregate([ + { $match: { _id: new mongoose.Types.ObjectId(ideaId) } }, + { $unwind: "$likeTimestamps" }, + { $match: { "likeTimestamps.likedAt": { $gte: startDate } } }, + { + $group: { + _id: { + $dateToString: { format: "%Y-%m-%d", date: "$likeTimestamps.likedAt" } + }, + count: { $sum: 1 } + } + }, + { $sort: { _id: 1 } } + ]); + + const today = new Date(); + const days = Array.from({ length: 30 }, (_, i) => { + const d = new Date(startDate); + d.setDate(d.getDate() + i); + return d.toISOString().split('T')[0]; + }); + + const resultMap = result.reduce((acc, entry) => { + acc[entry._id] = entry.count; + return acc; + }, {}); + + const final = days.map(date => ({ date, count: resultMap[date] || 0 })); + + res.json(final); + } catch (err) { + console.error('Error fetching like stats:', err); + res.status(500).json({ msg: 'Failed to fetch like stats' }); + } +}); + +/** + * ✅ Collaboration routes + */ router.post('/collaborate', auth, toggleCollaboration); router.get('/:ideaId/collaborators', auth, getCollaborators); @@ -139,4 +183,3 @@ router.put('/:ideaId/revert-collab', async (req, res) => { }); module.exports = router; - diff --git a/Startup-app/backend/routes/likeRoutes.js b/Startup-app/backend/routes/likeRoutes.js index 34da26a2..c431cbc1 100644 --- a/Startup-app/backend/routes/likeRoutes.js +++ b/Startup-app/backend/routes/likeRoutes.js @@ -8,44 +8,53 @@ router.post('/like', auth, async (req, res) => { const { ideaId } = req.body; const currentUserId = req.user.id; - console.log("🔠Like route hit"); - console.log("🧠ideaId:", ideaId); - console.log("👤 currentUserId:", currentUserId); - try { const idea = await Idea.findById(ideaId); - console.log("📌 Fetched Idea:", idea); if (!idea) { - console.log("⌠Idea not found"); return res.status(404).json({ msg: 'Idea not found' }); } + // Initialize likedBy and likeTimestamps if not present if (!Array.isArray(idea.likedBy)) { - console.log("🔧 likedBy was not an array, initializing..."); idea.likedBy = []; } + if (!idea.likeTimestamps) { + idea.likeTimestamps = new Map(); + } + const alreadyLiked = idea.likedBy.includes(currentUserId); - console.log("✅ Already liked?", alreadyLiked); + const today = new Date(); + const dateKey = today.toISOString().slice(0, 10); // yyyy-mm-dd if (alreadyLiked) { + // 👎 Unlike idea.likedBy = idea.likedBy.filter(id => id.toString() !== currentUserId.toString()); idea.likes = Math.max((idea.likes || 1) - 1, 0); + + const timestamps = idea.likeTimestamps.get(dateKey) || []; + const updated = timestamps.filter( + ts => new Date(ts).toISOString() !== today.toISOString() + ); + idea.likeTimestamps.set(dateKey, updated); } else { - idea.likedBy.push(currentUserId.toString()); + // 👠Like + idea.likedBy.push(currentUserId); idea.likes = (idea.likes || 0) + 1; + + const timestamps = idea.likeTimestamps.get(dateKey) || []; + timestamps.push(today); + idea.likeTimestamps.set(dateKey, timestamps); } await idea.save(); - console.log("💾 Idea saved"); res.status(200).json({ msg: alreadyLiked ? 'Idea unliked successfully' : 'Idea liked successfully', likes: idea.likes, likedBy: idea.likedBy }); - } catch (error) { console.error('🔥 Error toggling like:', error); return res.status(500).json({ msg: 'Server error' }); diff --git a/Startup-app/frontend/package-lock.json b/Startup-app/frontend/package-lock.json index a292113e..26d887a2 100644 --- a/Startup-app/frontend/package-lock.json +++ b/Startup-app/frontend/package-lock.json @@ -20,6 +20,7 @@ "react-icons": "^5.5.0", "react-router-dom": "^7.4.0", "react-scripts": "^5.0.1", + "recharts": "^2.15.3", "web-vitals": "^2.1.4" } }, @@ -3653,6 +3654,69 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "license": "MIT" }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -5636,6 +5700,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6372,6 +6445,133 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "license": "MIT" }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6466,6 +6666,12 @@ "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -6698,6 +6904,16 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -7938,6 +8154,15 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -9276,6 +9501,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -14110,6 +14344,37 @@ } } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -14145,6 +14410,44 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", + "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -16198,6 +16501,12 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -16721,6 +17030,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/Startup-app/frontend/package.json b/Startup-app/frontend/package.json index 89860847..cdf16b78 100644 --- a/Startup-app/frontend/package.json +++ b/Startup-app/frontend/package.json @@ -15,6 +15,7 @@ "react-icons": "^5.5.0", "react-router-dom": "^7.4.0", "react-scripts": "^5.0.1", + "recharts": "^2.15.3", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/Startup-app/frontend/src/components/IdeaOfTheDay.jsx b/Startup-app/frontend/src/components/IdeaOfTheDay.jsx index 23b8abdc..fa0de36b 100644 --- a/Startup-app/frontend/src/components/IdeaOfTheDay.jsx +++ b/Startup-app/frontend/src/components/IdeaOfTheDay.jsx @@ -24,7 +24,6 @@ const IdeaOfTheDay = () => { fetchIdea(); }, []); - // 🎉 Confetti only once per login session useEffect(() => { const alreadyShown = localStorage.getItem("confettiShown"); @@ -46,7 +45,6 @@ const IdeaOfTheDay = () => { }); }, 300); - // ✅ Mark as shown localStorage.setItem("confettiShown", "true"); } }, [idea]); @@ -66,7 +64,10 @@ const IdeaOfTheDay = () => { </div> <div className="text-purple-600 font-semibold text-sm mt-2"> - Likes: {idea.likes ?? 0} + 👠Likes: {idea.likes ?? 0} + </div> + <div className="text-purple-600 font-semibold text-sm mt-1"> + 🤠Collaborators: {idea.collaborators?.length ?? 0} </div> </div> diff --git a/Startup-app/frontend/src/components/LikesChart.jsx b/Startup-app/frontend/src/components/LikesChart.jsx new file mode 100644 index 00000000..7fdb0d57 --- /dev/null +++ b/Startup-app/frontend/src/components/LikesChart.jsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import "../styles/LikesChart.css"; + +const LikesChart = ({ ideaId }) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchLikes = async () => { + try { + const token = localStorage.getItem("token"); + const res = await axios.get( + `http://localhost:5001/api/ideas/my-posts`, + { headers: { "x-auth-token": token } } + ); + + const idea = res.data.find((post) => post._id === ideaId); + if (!idea || typeof idea.likeTimestamps !== "object") { + console.warn("⌠likeTimestamps missing or invalid for idea:", idea); + setLoading(false); + return; + } + + // Flatten all like timestamps from object + const timestampsArray = Object.values(idea.likeTimestamps).flat(); + + const likeCounts = {}; + timestampsArray.forEach((timestamp) => { + const dateKey = new Date(timestamp).toISOString().split("T")[0]; + likeCounts[dateKey] = (likeCounts[dateKey] || 0) + 1; + }); + + const today = new Date(); + const last30Days = Array.from({ length: 31 }, (_, i) => { + const date = new Date(); + date.setDate(today.getDate() - i); + return date.toISOString().split("T")[0]; + }).reverse(); + + const chartData = last30Days.map((date) => ({ + date, + likes: likeCounts[date] || 0, + })); + + setData(chartData); + } catch (err) { + console.error("Error fetching like chart data:", err); + } finally { + setLoading(false); + } + }; + + fetchLikes(); + }, [ideaId]); + + if (loading) return <div>Loading chart...</div>; + + return ( + <div className="likes-chart-popup"> + <button className="close-btn" onClick={() => window.location.reload()}>×</button> + <h3>📊 Likes Over the Last 30 Days</h3> + <ResponsiveContainer width="100%" height={300}> + <LineChart data={data} margin={{ top: 20, right: 30, left: 0, bottom: 5 }}> + <CartesianGrid strokeDasharray="3 3" /> + <XAxis dataKey="date" /> + <YAxis allowDecimals={false} /> + <Tooltip /> + <Line type="monotone" dataKey="likes" stroke="#8884d8" strokeWidth={2} /> + </LineChart> + </ResponsiveContainer> + </div> + ); +}; + +export default LikesChart; diff --git a/Startup-app/frontend/src/pages/MyPosts.js b/Startup-app/frontend/src/pages/MyPosts.js index 118bceed..820d281b 100644 --- a/Startup-app/frontend/src/pages/MyPosts.js +++ b/Startup-app/frontend/src/pages/MyPosts.js @@ -2,34 +2,16 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; import '../styles/Feed.css'; +import LikesChart from '../components/LikesChart'; import PostPopup from '../components/PostPopup'; const MyPosts = () => { const [posts, setPosts] = useState([]); const [selectedPost, setSelectedPost] = useState(null); + const [chartPost, setChartPost] = useState(null); 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 () => { @@ -41,18 +23,17 @@ const MyPosts = () => { } }); - // Initialize collaborators as an empty array for each post const postsWithCollaborators = await Promise.all( - response.data.map(async(post) => { + 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); @@ -60,17 +41,32 @@ const MyPosts = () => { setLoading(false); } }; + fetchMyPosts(); }, []); + 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}` } } + ); + setPosts((prevPosts) => + prevPosts.map((post) => + post._id === ideaId ? { ...post, collaborators: response.data } : post + ) + ); + } catch (error) { + console.error('Error fetching collaborators:', error.response?.data || error.message); + } + }; + return ( <div className="feed-container"> <div className="feed-header-row"> <h1>My Posts</h1> - <button - onClick={() => navigate('/post-idea')} - className="small-button" - > + <button onClick={() => navigate('/post-idea')} className="small-button"> + Add More </button> </div> @@ -82,21 +78,14 @@ const MyPosts = () => { ) : posts.length === 0 ? ( <div className="no-ideas"> <p>You haven't created any posts yet.</p> - <button - onClick={() => navigate('/post-idea')} - className="small-button" - > + <button onClick={() => navigate('/post-idea')} className="small-button"> Create your first post! </button> </div> ) : ( <div className="feed-grid"> {posts.map((post) => ( - <div - key={post._id} - className="idea-card" - onClick={() => setSelectedPost(post)} - > + <div key={post._id} className="idea-card" onClick={() => setSelectedPost(post)}> <div className="idea-header"> <h2 className="idea-title">{post.title}</h2> <p className="idea-author">Posted by you</p> @@ -125,7 +114,6 @@ const MyPosts = () => { <span>Location: {post.location}</span> </div> - {/* ✅ Styled list of users who liked the post */} {post.likedBy?.length > 0 && ( <div className="idea-meta liked-by-meta"> <span>Liked by:</span> @@ -138,21 +126,47 @@ const MyPosts = () => { )} <button - className="collaborators-btn" - onClick={(e) => { - e.stopPropagation(); - fetchCollaborators(post._id); - }} + className="collaborators-btn" + onClick={(e) => { + e.stopPropagation(); + fetchCollaborators(post._id); + }} > - Collaborators ({post.collaborators?.length || 0}) + 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} + + {post.collaborators?.length > 0 && ( + <ul> + {post.collaborators.map((collaborator) => ( + <li key={collaborator._id}> + {collaborator.name || collaborator.email} + </li> + ))} + </ul> + )} + + {/* ✅ Track Likes Button */} + <button + className="track-likes-btn" + style={{ + marginTop: '10px', + backgroundColor: '#6c63ff', // Deep purple background + color: 'white', // White text + fontWeight: 'bold', + padding: '8px 12px', + borderRadius: '8px', + border: 'none', + cursor: 'pointer', + boxShadow: '0 2px 5px rgba(0,0,0,0.1)' + }} + onClick={(e) => { + e.stopPropagation(); + setChartPost(post); + }} +> + 📈 Track Likes +</button> + </div> ))} </div> @@ -162,11 +176,22 @@ const MyPosts = () => { <PostPopup post={selectedPost} onClose={() => setSelectedPost(null)} - onAuthorClick={() => {}} // no navigation needed for own posts + onAuthorClick={() => {}} /> )} + + {chartPost && ( + <div className="custom-popup"> + <div className="custom-popup-inner" style={{ width: '80%', maxWidth: '900px' }}> + <button className="popup-close" onClick={() => setChartPost(null)}> + × + </button> + <LikesChart ideaId={chartPost._id} /> + </div> + </div> + )} </div> ); }; -export default MyPosts; \ No newline at end of file +export default MyPosts; diff --git a/Startup-app/frontend/src/styles/LikesChart.css b/Startup-app/frontend/src/styles/LikesChart.css new file mode 100644 index 00000000..39e05ca0 --- /dev/null +++ b/Startup-app/frontend/src/styles/LikesChart.css @@ -0,0 +1,35 @@ +.likes-chart-popup { + position: fixed; + top: 10%; + left: 50%; + transform: translateX(-50%); + background: white; + padding: 20px; + border-radius: 12px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); + z-index: 1000; + width: 90%; + max-width: 600px; + } + + .likes-chart-popup h3 { + text-align: center; + margin-bottom: 16px; + } + + .likes-chart-popup .close-btn { + background: #ff5c5c; + border: none; + color: white; + padding: 6px 10px; + border-radius: 6px; + cursor: pointer; + position: absolute; + right: 12px; + top: 12px; + } + + .likes-chart-popup .close-btn:hover { + background: #e74c3c; + } + \ No newline at end of file -- GitLab