Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/star-reminder.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 10 additions & 1 deletion backend/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
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 { 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());

Expand All @@ -17,6 +22,10 @@ app.use('/devdisplay/v1/trending/github', githubRouter);

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!',
Expand Down
96 changes: 96 additions & 0 deletions backend/controllers/ideaSubmission.controllers.js
Original file line number Diff line number Diff line change
@@ -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 });
}
};
34 changes: 34 additions & 0 deletions backend/controllers/winner.controller.js
Original file line number Diff line number Diff line change
@@ -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' });
}
};
31 changes: 31 additions & 0 deletions backend/cron/monthlyWinner.cron.js
Original file line number Diff line number Diff line change
@@ -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' },
);
1 change: 1 addition & 0 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 18 additions & 0 deletions backend/models/ideaSubmission.models.js
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 11 additions & 0 deletions backend/models/winner.models.js
Original file line number Diff line number Diff line change
@@ -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);
Loading