diff --git a/.github/workflows/star-reminder.yml b/.github/workflows/star-reminder.yml index 1902552f..ba0bc218 100644 --- a/.github/workflows/star-reminder.yml +++ b/.github/workflows/star-reminder.yml @@ -1,11 +1,9 @@ name: Star Reminder for Contributors on: - pull_request: + pull_request_target: types: [opened] - # pull_request_target: (for on every pr) - jobs: star-reminder: runs-on: ubuntu-latest diff --git a/backend/app.js b/backend/app.js index 0a5576f7..1433af5e 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,12 +1,18 @@ import express from 'express'; import { devRouter } from './routes/dev.routes.js'; import { githubRouter } from './routes/github.routes.js'; + import { subscribersRouter } from './routes/subscribers.routes.js'; import { ideasRouter } from './routes/ideas.routes.js'; import { getSubscribers } from './controllers/subscribers.controllers.js'; import Subscribers from './models/subscribers.models.js'; +import { ideaRouter } from './routes/ideaSubmission.routes.js'; +import { winnerRouter } from './routes/winner.routes.js'; +import cors from 'cors'; const app = express(); +app.use(cors()); + // Middleware to parse incoming JSON requests app.use(express.json()); @@ -21,6 +27,10 @@ app.use('/devdisplay/v1/ideas', ideasRouter); app.use('/devdisplay/v1/subscribers', getSubscribers); +// IdeaSubmission routes +app.use('/devdisplay/v1/idea-submissions', ideaRouter); +app.use('/devdisplay/v1/winners', winnerRouter); + app.get('/', (req, res) => { res.status(200).json({ message: 'Welcome to the DevDisplay API!', diff --git a/backend/controllers/ideaSubmission.controllers.js b/backend/controllers/ideaSubmission.controllers.js new file mode 100644 index 00000000..abf79008 --- /dev/null +++ b/backend/controllers/ideaSubmission.controllers.js @@ -0,0 +1,96 @@ +import IdeaSubmission from '../models/ideaSubmission.models.js'; + +const parseList = (val) => { + if (!val) return []; + try { + const parsed = JSON.parse(val); + return Array.isArray(parsed) ? parsed.map(String) : []; + } catch (error) { + return String(val) + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + } +}; + +export const createIdea = async (req, res) => { + try { + const title = req.body.tittle || req.body.title; + const description = req.body.description || ''; + + const tags = parseList(req.body.tags); + const resources = parseList(req.body.resources); + + const mediaUrls = (req.files || []).map((f) => `/uploads/${f.filename}`); + + if (!title || !title.trim()) { + return res.status(400).json({ error: 'Title is required' }); + } + + const idea = await IdeaSubmission.create({ + title: title.trim(), + description, + tags, + resources, + mediaUrls, + }); + + res.status(201).json({ message: 'Idea created', idea }); + } catch (err) { + console.error('Create idea error:', err); + res.status(500).json({ error: 'Server error' }); + } +}; + +export const getIdeas = async (_req, res) => { + try { + const ideas = await IdeaSubmission.find().sort({ createdAt: -1 }); + res.json(ideas); + } catch (err) { + console.error('Fetch ideas error:', err); + res.status(500).json({ error: 'Server error' }); + } +}; + +export const getIdeaById = async (req, res) => { + try { + const idea = await IdeaSubmission.findById(req.params.id); + if (!idea) return res.status(404).json({ error: 'Not found' }); + res.json(idea); + } catch (err) { + res.status(400).json({ error: 'Invalid ID' }); + } +}; + +export const voteIdea = async (req, res) => { + try { + const { id } = req.params; + const userId = req.ip; + + const idea = await IdeaSubmission.findById(id); + if (!idea) { + return res.status(404).json({ error: 'Idea not found' }); + } + + const index = idea.voters.indexOf(userId); + + if (index === -1) { + idea.voters.push(userId); + idea.votes += 1; + } else { + idea.voters.splice(index, 1); + idea.votes -= 1; + } + + await idea.save(); + + res.json({ + message: 'Vote toggled', + votes: idea.votes, + voters: idea.voters, + idea, + }); + } catch (err) { + res.status(500).json({ error: 'Server error', details: err.message }); + } +}; diff --git a/backend/controllers/winner.controller.js b/backend/controllers/winner.controller.js new file mode 100644 index 00000000..e48a1bb5 --- /dev/null +++ b/backend/controllers/winner.controller.js @@ -0,0 +1,34 @@ +import winnerModels from '../models/winner.models.js'; +import { selectWinnerForMonth } from '../services/winner.service.js'; + +export const getLatestWinner = async (req, res) => { + try { + const latest = await winnerModels.findOne().sort({ createdAt: -1 }).populate('ideaId'); + + if (!latest) return res.json({ message: 'No winner found' }); + res.json(latest); + } catch (error) { + console.error('Error fetching latest winner:', error); + res.status(500).json({ message: 'Internal server error' }); + } +}; + +export const selectWinner = async (req, res) => { + try { + const now = new Date(); + const year = Number(req.query.year ?? now.getFullYear()); + const month = Number(req.query.month ?? now.getMonth()); + + const winner = await selectWinnerForMonth(year, month); + + if (!winner) { + return res.status(404).json({ message: 'No ideas found for the specified month' }); + } + + const populated = await winner.populate('ideaId'); // βœ… fixed field + res.json({ message: 'Winner selected', winner: populated }); + } catch (e) { + console.error('Error selecting winner:', e); + res.status(500).json({ message: 'Internal server error' }); + } +}; diff --git a/backend/cron/monthlyWinner.cron.js b/backend/cron/monthlyWinner.cron.js new file mode 100644 index 00000000..b47250e2 --- /dev/null +++ b/backend/cron/monthlyWinner.cron.js @@ -0,0 +1,31 @@ +import cron from 'node-cron'; +import { selectWinnerForMonth } from '../services/winner.service.js'; + +cron.schedule( + '59 23 28-31 * *', + async () => { + const now = new Date(); + const tomorrow = new Date(now); + tomorrow.setDate(now.getDate() + 1); + + if (tomorrow.getMonth() !== now.getMonth()) { + const year = now.getFullYear(); + const month = now.getMonth(); + + console.log(`Running end-of-month winner selection for ${year}-${month + 1}`); + + try { + const winner = await selectWinnerForMonth(year, month); + + if (!winner) { + console.log('No eligible ideas to select.'); + } else { + console.log('Winner selected:', winner._id.toString()); + } + } catch (error) { + console.error(`Error occurred while selecting monthly winner for ${year}-${month + 1}:`, error); + } + } + }, + { timezone: 'Asia/Kolkata' }, +); diff --git a/backend/index.js b/backend/index.js index a865553c..c61e3d84 100644 --- a/backend/index.js +++ b/backend/index.js @@ -4,6 +4,7 @@ import dotenv from 'dotenv'; import pingDB from './cron/test.cron.js'; import * as devCronJobs from './cron/dev.cron.js'; import * as githubCronJobs from './cron/github.cron.js'; +import * as monthlyWinnerCron from './cron/monthlyWinner.cron.js'; dotenv.config({ path: './.env', diff --git a/backend/models/ideaSubmission.models.js b/backend/models/ideaSubmission.models.js new file mode 100644 index 00000000..dc6a3b07 --- /dev/null +++ b/backend/models/ideaSubmission.models.js @@ -0,0 +1,18 @@ +import mongoose from 'mongoose'; + +const IdeaSchema = new mongoose.Schema( + { + title: { type: String, required: true, trim: true, minlength: 2, maxlength: 140 }, + description: { type: String, default: '' }, + tags: { type: [String], default: [] }, + resources: { type: [String], default: [] }, + mediaUrls: { type: [String], default: [] }, + votes: { type: Number, default: 0 }, + voters: [{ type: String }], + }, + { timestamps: true }, +); + +const IdeaSubmission = mongoose.model('IdeaSubmission', IdeaSchema); + +export default IdeaSubmission; diff --git a/backend/models/winner.models.js b/backend/models/winner.models.js new file mode 100644 index 00000000..3b239509 --- /dev/null +++ b/backend/models/winner.models.js @@ -0,0 +1,11 @@ +import mongoose from 'mongoose'; + +const winnerSchema = new mongoose.Schema({ + ideaId: { type: mongoose.Schema.Types.ObjectId, ref: 'IdeaSubmission', required: true }, + month: { type: Number, required: true }, + year: { type: Number, required: true }, + createdAt: { type: Date, default: Date.now }, +}); + +winnerSchema.index({ month: 1, year: 1 }, { unique: true }); +export default mongoose.model('Winner', winnerSchema); diff --git a/backend/package-lock.json b/backend/package-lock.json index 43e88a6d..d2bdbd93 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,10 +11,12 @@ "dependencies": { "axios": "^1.8.2", "cheerio": "^1.0.0", + "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", "mongoose": "^8.12.1", "mongoose-aggregate-paginate-v2": "^1.1.4", + "multer": "^2.0.2", "node-cron": "^3.0.3" }, "devDependencies": { @@ -75,6 +77,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -193,6 +200,22 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -317,6 +340,20 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -353,6 +390,18 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -1076,6 +1125,25 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mongodb": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", @@ -1219,6 +1287,23 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1232,7 +1317,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", - "license": "ISC", "dependencies": { "uuid": "8.3.2" }, @@ -1316,6 +1400,14 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -1491,6 +1583,19 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1712,6 +1817,22 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1782,6 +1903,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1807,6 +1933,11 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1876,6 +2007,14 @@ "engines": { "node": ">=18" } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } } } } diff --git a/backend/package.json b/backend/package.json index 9f5c9fb0..5e560f90 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,10 +16,12 @@ "dependencies": { "axios": "^1.8.2", "cheerio": "^1.0.0", + "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", "mongoose": "^8.12.1", "mongoose-aggregate-paginate-v2": "^1.1.4", + "multer": "^2.0.2", "node-cron": "^3.0.3" }, "devDependencies": { diff --git a/backend/routes/ideaSubmission.routes.js b/backend/routes/ideaSubmission.routes.js new file mode 100644 index 00000000..5e7b9a3a --- /dev/null +++ b/backend/routes/ideaSubmission.routes.js @@ -0,0 +1,30 @@ +import express from 'express'; +import multer from 'multer'; +import path from 'path'; +import { createIdea, getIdeaById, getIdeas, voteIdea } from '../controllers/ideaSubmission.controllers.js'; + +const router = express.Router(); + +const uploadDir = path.join(process.cwd(), 'uploads'); +const storage = multer.diskStorage({ + destination: (req, file, cb) => cb(null, uploadDir), + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + const base = path.basename(file.originalname, ext); + cb(null, `${Date.now()}-${base}${ext}`); + }, +}); +const fileFilter = (_req, file, cb) => { + const allowed = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp', 'image/gif']; + if (allowed.includes(file.mimetype)) cb(null, true); + else cb(new Error('Only image files are allowed')); +}; +const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 } }); + +// Routes +router.post('/', upload.array('media', 10), createIdea); +router.get('/', getIdeas); +router.get('/:id', getIdeaById); +router.post('/:id/vote', voteIdea); + +export { router as ideaRouter }; diff --git a/backend/routes/winner.routes.js b/backend/routes/winner.routes.js new file mode 100644 index 00000000..54629de4 --- /dev/null +++ b/backend/routes/winner.routes.js @@ -0,0 +1,10 @@ +import express from 'express'; + +import { getLatestWinner, selectWinner } from '../controllers/winner.controller.js'; + +const router = express.Router(); + +router.get('/latest', getLatestWinner); +router.post('/select', selectWinner); + +export { router as winnerRouter }; diff --git a/backend/services/winner.service.js b/backend/services/winner.service.js new file mode 100644 index 00000000..a4cb6b1f --- /dev/null +++ b/backend/services/winner.service.js @@ -0,0 +1,24 @@ +import winnerModels from '../models/winner.models.js'; +import IdeaSubmission from '../models/ideaSubmission.models.js'; + +export async function selectWinnerForMonth(year, month) { + const start = new Date(year, month, 1, 0, 0, 0, 0); + const end = new Date(year, month + 1, 1, 0, 0, 0, 0); + + const existing = await winnerModels.findOne({ month, year }); + if (existing) return existing; + + const topIdea = await IdeaSubmission.find({ createdAt: { $gte: start, $lt: end } }) + .sort({ votes: -1 }) + .limit(1); + + if (!topIdea.length) return null; + + const winner = await winnerModels.create({ + ideaId: topIdea[0]._id, + month, + year, + }); + + return winner; +} diff --git a/src/App.js b/src/App.js index 0b18e10f..c5b4e439 100644 --- a/src/App.js +++ b/src/App.js @@ -56,7 +56,7 @@ import DevShare from './Page/ResoucesHub/DevShare.jsx'; import PageNotFound from './Page/PageNotFound.jsx'; import ProfilePage from './components/Profile/ProfilePage'; import { ResumeProvider } from './components/ResumeBuilder/context/ResumeContext.jsx'; - +import Collaboration from './Page/Collaboration.jsx'; function App() { React.useEffect(() => { document.documentElement.classList.add('dark'); @@ -116,6 +116,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/Homepage.jsx b/src/Homepage.jsx index 7c3651a8..9f1f7408 100644 --- a/src/Homepage.jsx +++ b/src/Homepage.jsx @@ -12,7 +12,7 @@ import filenames from './ProfilesList.json'; function App() { const profilesRef = useRef(); - const [profiles, setProfiles] = useState([]); + const [profiles, setProfiles] = useState([]); //profiles const [searching, setSearching] = useState(false); const [combinedData, setCombinedData] = useState([]); const [currentPage, setCurrentPage] = useState(1); diff --git a/src/Page/Collaboration.jsx b/src/Page/Collaboration.jsx new file mode 100644 index 00000000..14b2b7b0 --- /dev/null +++ b/src/Page/Collaboration.jsx @@ -0,0 +1,145 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { Link } from 'react-router-dom'; +import RoleAssignment from '../components/RoleAssignment'; + +const API_BASE = 'http://localhost:5000/devdisplay/v1'; +const Collaboration = () => { + const { id } = useParams(); + const [idea, setIdea] = useState(null); + const [loading, setLoading] = useState(true); + const [err, setErr] = useState(''); + + useEffect(() => { + let mounted = true; + (async () => { + try { + const res = await fetch(`${API_BASE}/idea-submissions/${id}`); + const data = await res.json(); + if (!res.ok) throw new Error(data?.error || 'Failed to load idea'); + if (mounted) setIdea(data); + } catch (error) { + if (mounted) setErr(error.message || 'Error loading idea'); + } finally { + if (mounted) setLoading(false); + } + })(); + return () => { + mounted = false; + }; + }, [id]); + + if (loading) { + return ( +
+
+
+
+
+
+
+ ); + } + + if (err || !idea) { + return ( +
+
{err || 'Idea not found.'}
+ + Go back + +
+ ); + } + + const tags = Array.isArray(idea.tags) ? idea.tags : []; + const created = idea.createdAt ? new Date(idea.createdAt).toLocaleDateString() : ''; + return ( +
+
+

+ Brightest Idea to work on{' '} +

+
+ + ← Back + + +
+
+ {/* Header */} +
+

{idea.title}

+

{idea.description}

+
+ {tags.map((t, i) => ( + + {t} + + ))} +
+
+ Created: {created} β€’ Votes: {idea.votes ?? 0} +
+
+ + {/* Grid Layout */} +
+ {/* Task Board */} +
+

πŸ“‹ Task Board

+
+ {['To Do', 'In Progress', 'Done'].map((col) => ( +
+

{col}

+
+
Design Wireframe
+
API Integration
+
+
+ ))} +
+
+ + {/* Roles Section */} +
+ +
+ + {/* Chatroom */} +
+

πŸ’¬ Community Chat

+
+
+ Rupali: Let’s start with UI. +
+
+ Dev1: Sure! I’ll handle backend setup. +
+
+ +
+ + {/* Progress Tracker */} +
+

πŸ“Š Progress Tracker

+
+
+
+

45% Completed

+
+
+
+ ); +}; + +export default Collaboration; diff --git a/src/Page/IdeaSubmission.jsx b/src/Page/IdeaSubmission.jsx index db387fa2..e138b3f7 100644 --- a/src/Page/IdeaSubmission.jsx +++ b/src/Page/IdeaSubmission.jsx @@ -1,221 +1,207 @@ -import React, { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; -import IdeaSubmissionForm from '../components/IdeaSubmission/IdeaSubmissionForm'; -import IdeaCard from '../components/IdeaSubmission/IdeaCard'; -import TrendingIdeas from '../components/IdeaSubmission/TrendingIdeas'; -import SubmissionStatus from '../components/IdeaSubmission/SubmissionStatus'; -import CollaborationHub from '../components/IdeaSubmission/CollaborationHub'; +import React, { useEffect, useState } from 'react'; +import { FaCalendar, FaLightbulb, FaThumbsUp, FaUser } from 'react-icons/fa'; +import { FiThumbsUp, FiTrendingUp } from 'react-icons/fi'; +import IdeaSubmissionForm from '../components/IdeaSubmissionForm'; +import MonthlyWinner from './MonthlyWinner'; const IdeaSubmissionPage = () => { - const [activeTab, setActiveTab] = useState('submit'); + const [time, setTime] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 }); + const [isFirstWeek, setIsFirstWeek] = useState(true); + const [showForm, setShowForm] = useState(false); const [ideas, setIdeas] = useState([]); - const [trendingIdeas, setTrendingIdeas] = useState([]); - const [submissionStatus, setSubmissionStatus] = useState(null); - const [loading, setLoading] = useState(true); - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - - const tabs = [ - { id: 'submit', label: 'πŸ’‘ Submit Idea', icon: 'πŸš€' }, - { id: 'browse', label: 'πŸ“‹ Browse Ideas', icon: 'πŸ‘€' }, - { id: 'trending', label: 'πŸ”₯ Trending', icon: 'πŸ“ˆ' }, - { id: 'collaborate', label: '🀝 Collaborate', icon: '⚑' }, - ]; - useEffect(() => { - fetchSubmissionStatus(); - fetchTrendingIdeas(); - if (activeTab === 'browse') { - fetchCurrentMonthIdeas(); - } - }, [activeTab, currentPage]); - - const fetchSubmissionStatus = async () => { + const handleVote = async (ideaId) => { try { - const response = await fetch('/devdisplay/v1/ideas/status'); - const data = await response.json(); - if (data.success) { - setSubmissionStatus(data.data); - } - } catch (error) { - console.error('Error fetching submission status:', error); + const res = await fetch(`http://localhost:5000/devdisplay/v1/idea-submissions/${ideaId}/vote`, { + method: 'POST', + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to vote'); + + setIdeas((prevIdeas) => prevIdeas.map((idea) => (idea._id === ideaId ? { ...idea, votes: data.votes } : idea))); + } catch (err) { + alert(err.message); } }; - const fetchTrendingIdeas = async () => { - try { - const response = await fetch('/devdisplay/v1/ideas/trending?limit=5'); - const data = await response.json(); - if (data.success) { - setTrendingIdeas(data.data); + // Fetch existing ideas from backend + useEffect(() => { + const fetchIdeas = async () => { + try { + const res = await fetch('http://localhost:5000/devdisplay/v1/idea-submissions/'); + const data = await res.json(); + setIdeas(data); + } catch (err) { + console.log('Error fetching ideas:', err); } - } catch (error) { - console.error('Error fetching trending ideas:', error); - } - }; + }; + fetchIdeas(); + }, []); - const fetchCurrentMonthIdeas = async () => { - setLoading(true); - try { - const response = await fetch(`/devdisplay/v1/ideas/current?page=${currentPage}&limit=10`); - const data = await response.json(); - if (data.success) { - setIdeas(data.data.ideas); - setTotalPages(data.data.totalPages); + // Timer logic + useEffect(() => { + const updateTimer = () => { + const now = new Date(); + const dayOfMonth = now.getDate(); + const inFirstWeek = dayOfMonth <= 7; + setIsFirstWeek(inFirstWeek); + + let deadline; + if (inFirstWeek) { + deadline = new Date(now.getFullYear(), now.getMonth(), 7, 23, 59, 59); + } else { + deadline = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); } - } catch (error) { - console.error('Error fetching ideas:', error); - } finally { - setLoading(false); - } - }; - const handleIdeaSubmitted = () => { - fetchSubmissionStatus(); - fetchTrendingIdeas(); - if (activeTab === 'browse') { - fetchCurrentMonthIdeas(); - } - }; + const diff = deadline - now; + if (diff <= 0) { + setTime({ days: 0, hours: 0, minutes: 0, seconds: 0 }); + return; + } - const handleVoteUpdate = (ideaId, newVoteCount) => { - setIdeas((prevIdeas) => prevIdeas.map((idea) => (idea._id === ideaId ? { ...idea, votes: newVoteCount } : idea))); - fetchTrendingIdeas(); // Update trending as well - }; + setTime({ + days: Math.floor(diff / (1000 * 60 * 60 * 24)), + hours: Math.floor((diff / (1000 * 60 * 60)) % 24), + minutes: Math.floor((diff / (1000 * 60)) % 60), + seconds: Math.floor((diff / 1000) % 60), + }); + }; + + updateTimer(); + const timer = setInterval(updateTimer, 1000); + return () => clearInterval(timer); + }, []); return ( -
- {/* Header */} -
-
- - πŸ’‘ Idea Submission Hub - - - Submit your innovative project ideas, vote for favorites, and collaborate with the community to bring - top-voted ideas to life! - - - {/* Submission Status Banner */} - {submissionStatus && } +
+ {/* Navbar */} + +
+ + + + {/* Submission Section */} + {isFirstWeek && ( +
+
Time Left for Submissions
+
Submissions for this month close at the end of the first week!
+
+ {['days', 'hours', 'minutes', 'seconds'].map((unit) => ( +
+
{time[unit]}
+
{unit.charAt(0).toUpperCase() + unit.slice(1)}
+
+ ))} +
+
+
setShowForm(true)} + > + +
Submit your idea
+
+
+ {showForm && ( + setShowForm(false)} + onSubmitSuccess={(newIdea) => setIdeas([newIdea, ...ideas])} + /> + )}
-
- - {/* Navigation Tabs */} -
-
-
- {tabs.map((tab) => ( - + )} + + {/* Voting Timer Section */} + {!isFirstWeek && ( +
+
Voting Closes In!
+
+ Vote for your favorite ideas before the month ends. The next submission round starts next month. +
+
+ {['days', 'hours', 'minutes', 'seconds'].map((unit) => ( +
+
{time[unit]}
+
{unit.charAt(0).toUpperCase() + unit.slice(1)}
+
))}
-
+ )} - {/* Content */} -
- {activeTab === 'submit' && ( - - - - )} - - {activeTab === 'browse' && ( - -
-

πŸ“‹ Browse Current Month Ideas

- {submissionStatus && ( -

Submission Period: {submissionStatus.submissionPeriod}

- )} -
+
+ +
+ {/* Trending Now Section */} +
+
+ +
Trending Now
+
- {loading ? ( -
- {[1, 2, 3, 4, 5, 6].map((i) => ( -
-
-
-
+ {ideas + .slice() + .sort((a, b) => b.votes - a.votes) + .slice(0, 5) + .map((idea, index) => ( +
+
{index + 1}.
+
+
{idea.title}
+
+ +
{idea.votes} votes
- ))} -
- ) : ideas.length > 0 ? ( - <> -
- {ideas.map((idea) => ( - - ))}
+
+ ))} +
- {/* Pagination */} - {totalPages > 1 && ( -
- - - Page {currentPage} of {totalPages} + {/* Ideas Section */} +
+
Ideas
+ +
+ {ideas.map((idea) => ( +
+ {/*
+
+ +
+
+
{idea.title || 'Anonymous'}
+
{new Date(idea.createdAt).toLocaleString()}
+
+
*/} +
{idea.title}
+
{idea.Description}
+
+ {idea.tags?.map((tag, idx) => ( + + {tag} - + ))} +
+
+
+ +
{new Date(idea.createdAt).toLocaleDateString()}
- )} - - ) : ( -
-
πŸ’‘
-

No Ideas Yet

-

Be the first to submit an innovative idea this month!

+
+ handleVote(idea._id)} color="gray" className="cursor-pointer" /> +
{idea.votes || 0}
+
+
- )} - - )} - - {activeTab === 'trending' && ( - - - - )} - - {activeTab === 'collaborate' && ( - - - - )} + ))} + {ideas.length === 0 &&
No ideas submitted yet.
} +
+
); diff --git a/src/Page/MonthlyWinner.jsx b/src/Page/MonthlyWinner.jsx new file mode 100644 index 00000000..ec6622a3 --- /dev/null +++ b/src/Page/MonthlyWinner.jsx @@ -0,0 +1,49 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const MonthlyWinner = () => { + const [winnerData, setWinnerData] = useState(null); + const navigate = useNavigate(); + useEffect(() => { + const fetchWinner = async () => { + try { + const res = await fetch('http://localhost:5000/devdisplay/v1/winners/latest'); + const data = await res.json(); + console.log('Fetched Winner Data:', data); + setWinnerData(data); + } catch (err) { + console.log('Error fetching monthly winner:', err); + } + }; + fetchWinner(); + }, []); + + // If no winner yet + if (!winnerData || !winnerData.ideaId) { + return ( +
Winner will be announced at the end of the month.
+ ); + } + + const { _id, title, description, votes } = winnerData.ideaId; + const openCollabPage = () => { + window.open(`/collaboration/${_id}`, '_blank', 'noopener,noreferrer'); + }; + + return ( +
+

πŸ† Monthly Winner

+
+
{title}
+
{description}
+
Votes: {votes}
+
+
+ ); +}; + +export default MonthlyWinner; diff --git a/src/components/IdeaSubmissionForm.jsx b/src/components/IdeaSubmissionForm.jsx new file mode 100644 index 00000000..b503634a --- /dev/null +++ b/src/components/IdeaSubmissionForm.jsx @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import { FaTimes } from 'react-icons/fa'; + +const IdeaSubmissionForm = ({ onClose }) => { + const [tittle, setTittle] = useState(''); + const [Description, setDescription] = useState(''); + const [tags, setTags] = useState([]); + const [Resources, setResources] = useState([]); + const [media, setMedia] = useState([]); + const [mediaFiles, setMediaFiles] = useState([]); + + const handleImageChange = (e) => { + const files = Array.from(e.target.files); + setMediaFiles(files); + setMedia(files.map((file) => URL.createObjectURL(file))); + }; + + const onSubmitHandler = async (e) => { + e.preventDefault(); + const formData = new FormData(); + formData.append('tittle', tittle); + formData.append('description', Description); + formData.append('tags', JSON.stringify(tags)); + formData.append('resources', JSON.stringify(Resources)); + + media.forEach((file) => formData.append('media', file)); + + try { + const res = await fetch('http://localhost:5000/devdisplay/v1/idea-submissions/', { + method: 'POST', + body: formData, + }); + const data = await res.json(); + if (onClose) onClose(); + } catch (err) { + console.log('Error submitting idea:', err); + } + console.log({ tittle, Description, tags, Resources, media }); + if (onClose) onClose(); + }; + + return ( +
+
+
onClose()} className="flex cursor-pointer items-center justify-between"> +
Idea Submission Form
+
+ +
+
+ +
+ setTittle(e.target.value)} + required + className="rounded-md p-2" + /> + + + + setTags(e.target.value.split(','))} + className="rounded-md p-2" + /> + + setResources(e.target.value.split(','))} + className="rounded-md p-2" + /> + + {/* Media Upload */} + + + + +
+
+
+ ); +}; + +export default IdeaSubmissionForm; diff --git a/src/components/ResumeBuilder/utils/pdfGenerator.js b/src/components/ResumeBuilder/utils/pdfGenerator.js index afc3bb18..1b168d3c 100644 --- a/src/components/ResumeBuilder/utils/pdfGenerator.js +++ b/src/components/ResumeBuilder/utils/pdfGenerator.js @@ -50,7 +50,7 @@ const renderSection = (doc, yPosition, margin, pageWidth, contentWidth, title, d doc.setDrawColor(150); doc.setLineWidth(0.5); - doc.line(margin, yPosition, margin + contentWidth, yPosition); + doc.line(margin, yPosition, margin + contentWidth, yPosition); // draw line under title yPosition += 4; // uniform gap after the line diff --git a/src/components/RoleAssignment.jsx b/src/components/RoleAssignment.jsx new file mode 100644 index 00000000..e466eb09 --- /dev/null +++ b/src/components/RoleAssignment.jsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; + +const RoleAssignment = () => { + const defaultRoles = ['Project Lead', 'Developer', 'Designer', 'Tester']; + const [roles, setRoles] = useState(defaultRoles.map((role) => ({ name: role, assignedTo: null }))); + const [customRole, setCustomRole] = useState(''); + const [selectedRole, setSelectedRole] = useState(''); + + const handleAssign = (role) => { + if (!role) return; + setRoles((prev) => prev.map((r) => (r.name === role && !r.assignedTo ? { ...r, assignedTo: 'You' } : r))); + setSelectedRole(''); + setCustomRole(''); + }; + + const handleAddCustomRole = () => { + if (!customRole.trim()) return; + setRoles((prev) => [...prev, { name: customRole, assignedTo: 'You' }]); + setCustomRole(''); + setSelectedRole(''); + }; + + return ( +
+

πŸ‘₯ Role Assignment

+ + {/* If no role assigned yet */} + {roles.every((r) => !r.assignedTo) &&

No roles have been assigned yet.

} + +
+ {roles.map((role, index) => ( +
+ {role.name} + {role.assignedTo ? ( + Assigned to {role.assignedTo} + ) : ( + + )} +
+ ))} +
+ + {/* Other Role Option */} +
+

Add Custom Role

+
+ setCustomRole(e.target.value)} + placeholder="Enter custom role" + className="flex-1 rounded border px-3 py-2" + /> + +
+
+
+ ); +}; + +export default RoleAssignment;